Skip to content
体验新版
项目
组织
正在加载...
登录
切换导航
打开侧边栏
LinuxSuRen
jenkins
提交
5f51089b
J
jenkins
项目概览
LinuxSuRen
/
jenkins
与 Fork 源项目一致
从无法访问的项目Fork
通知
2
Star
0
Fork
0
代码
文件
提交
分支
Tags
贡献者
分支图
Diff
Issue
0
列表
看板
标记
里程碑
合并请求
0
Wiki
0
Wiki
分析
仓库
DevOps
项目成员
Pages
J
jenkins
项目概览
项目概览
详情
发布
仓库
仓库
文件
提交
分支
标签
贡献者
分支图
比较
Issue
0
Issue
0
列表
看板
标记
里程碑
合并请求
0
合并请求
0
Pages
分析
分析
仓库分析
DevOps
Wiki
0
Wiki
成员
成员
收起侧边栏
关闭侧边栏
动态
分支图
创建新Issue
提交
Issue看板
体验新版 GitCode,发现更多精彩内容 >>
提交
5f51089b
编写于
5月 24, 2012
作者:
K
Kohsuke Kawaguchi
浏览文件
操作
浏览文件
下载
电子邮件补丁
差异文件
Generalized AgentProtocol into an extension point.
上级
25d990c5
变更
7
隐藏空白更改
内联
并排
Showing
7 changed file
with
475 addition
and
252 deletion
+475
-252
changelog.html
changelog.html
+2
-0
core/src/main/java/hudson/TcpSlaveAgentListener.java
core/src/main/java/hudson/TcpSlaveAgentListener.java
+13
-252
core/src/main/java/hudson/cli/CliProtocol.java
core/src/main/java/hudson/cli/CliProtocol.java
+55
-0
core/src/main/java/hudson/cli/CliProtocol2.java
core/src/main/java/hudson/cli/CliProtocol2.java
+79
-0
core/src/main/java/jenkins/AgentProtocol.java
core/src/main/java/jenkins/AgentProtocol.java
+52
-0
core/src/main/java/jenkins/slaves/JnlpSlaveAgentProtocol.java
.../src/main/java/jenkins/slaves/JnlpSlaveAgentProtocol.java
+153
-0
core/src/main/java/jenkins/slaves/JnlpSlaveAgentProtocol2.java
...src/main/java/jenkins/slaves/JnlpSlaveAgentProtocol2.java
+121
-0
未找到文件。
changelog.html
浏览文件 @
5f51089b
...
...
@@ -83,6 +83,8 @@ Upcoming changes</a>
Added more context menus to hyperlinks in the console output
<li
class=
rfe
>
Exposed plugin manager and update center to the REST API
<li
class=
rfe
>
Added a new extension point for agent protocols.
<li
class=
rfe
>
Enabled concurrent build support for matrix projects
(
<a
href=
"https://issues.jenkins-ci.org/browse/JENKINS-6747"
>
issue 6747
</a>
)
...
...
core/src/main/java/hudson/TcpSlaveAgentListener.java
浏览文件 @
5f51089b
...
...
@@ -23,81 +23,33 @@
*/
package
hudson
;
import
hudson.cli.Connection
;
import
hudson.util.Secret
;
import
jenkins.model.Jenkins
;
import
hudson.model.Computer
;
import
hudson.slaves.OfflineCause
;
import
hudson.slaves.SlaveComputer
;
import
hudson.remoting.Channel
;
import
hudson.remoting.SocketOutputStream
;
import
hudson.remoting.SocketInputStream
;
import
hudson.remoting.Engine
;
import
hudson.remoting.Channel.Listener
;
import
hudson.remoting.Channel.Mode
;
import
hudson.cli.CliManagerImpl
;
import
hudson.cli.CliEntryPoint
;
import
hudson.util.IOException2
;
import
jenkins.AgentProtocol
;
import
javax.crypto.Cipher
;
import
javax.crypto.CipherInputStream
;
import
javax.crypto.CipherOutputStream
;
import
javax.crypto.SecretKey
;
import
javax.crypto.spec.IvParameterSpec
;
import
javax.crypto.spec.SecretKeySpec
;
import
java.io.ByteArrayInputStream
;
import
java.io.DataInputStream
;
import
java.io.DataOutputStream
;
import
java.io.IOException
;
import
java.io.InputStream
;
import
java.io.OutputStream
;
import
java.io.PrintWriter
;
import
java.io.BufferedInputStream
;
import
java.io.BufferedOutputStream
;
import
java.lang.reflect.InvocationTargetException
;
import
java.lang.reflect.Method
;
import
java.net.BindException
;
import
java.net.ServerSocket
;
import
java.net.Socket
;
import
java.net.BindException
;
import
java.security.GeneralSecurityException
;
import
java.security.PrivateKey
;
import
java.security.PublicKey
;
import
java.security.SecureRandom
;
import
java.security.Signature
;
import
java.util.Map.Entry
;
import
java.util.Properties
;
import
java.util.concurrent.ExecutionException
;
import
java.util.concurrent.TimeUnit
;
import
java.util.concurrent.TimeoutException
;
import
java.util.logging.Level
;
import
java.util.logging.Logger
;
/**
* Listens to incoming TCP connections from JNLP slave agents and CLI.
*
* <h2>Security</h2>
* <p>
* Once connected, remote slave agents can send in commands to be
* executed on the master, so in a way this is like an rsh service.
* Therefore, it is important that we reject connections from
* unauthorized remote slaves.
*
* <p>
* The approach here is to have {@link jenkins.model.Jenkins#getSecretKey() a secret key} on the master.
* This key is sent to the slave inside the <tt>.jnlp</tt> file
* (this file itself is protected by HTTP form-based authentication that
* we use everywhere else in Hudson), and the slave sends this
* token back when it connects to the master.
* Unauthorized slaves can't access the protected <tt>.jnlp</tt> file,
* so it can't impersonate a valid slave.
* Aside from the HTTP endpoint, Jenkins runs {@link TcpSlaveAgentListener} that listens on a TCP socket.
* Historically this was used for inbound connection from slave agents (hence the name), but over time
* it was extended and made generic, so that multiple protocols of different purposes can co-exist on the
* same socket.
*
* <p>
* We don't want to force the JNLP slave agents to be restarted
* whenever the server restarts, so right now this secret master key
* is generated once and used forever, which makes this whole scheme
* less secure.
* This class accepts the socket, then after a short handshaking, it dispatches to appropriate
* {@link AgentProtocol}s.
*
* @author Kohsuke Kawaguchi
* @see AgentProtocol
*/
public
final
class
TcpSlaveAgentListener
extends
Thread
{
...
...
@@ -131,10 +83,6 @@ public final class TcpSlaveAgentListener extends Thread {
return
serverSocket
.
getLocalPort
();
}
private
String
getSecretKey
()
{
return
Jenkins
.
getInstance
().
getSecretKey
();
}
@Override
public
void
run
()
{
try
{
...
...
@@ -195,17 +143,11 @@ public final class TcpSlaveAgentListener extends Thread {
if
(
s
.
startsWith
(
"Protocol:"
))
{
String
protocol
=
s
.
substring
(
9
);
if
(
protocol
.
equals
(
"JNLP-connect"
))
{
runJnlpConnect
(
in
,
out
);
}
else
if
(
protocol
.
equals
(
"JNLP2-connect"
))
{
runJnlp2Connect
(
in
,
out
);
}
else
if
(
protocol
.
equals
(
"CLI-connect"
))
{
runCliConnect
(
in
,
out
);
}
else
if
(
protocol
.
equals
(
"CLI2-connect"
))
{
runCliConnect2
();
}
else
{
AgentProtocol
p
=
AgentProtocol
.
of
(
protocol
);
if
(
p
!=
null
)
p
.
handle
(
this
.
s
);
else
error
(
out
,
"Unknown protocol:"
+
s
);
}
}
else
{
error
(
out
,
"Unrecognized protocol: "
+
s
);
}
...
...
@@ -226,185 +168,6 @@ public final class TcpSlaveAgentListener extends Thread {
}
}
/**
* Handles CLI connection request.
*/
private
void
runCliConnect
(
DataInputStream
in
,
PrintWriter
out
)
throws
IOException
,
InterruptedException
{
out
.
println
(
"Welcome"
);
runCli
(
new
Connection
(
s
));
}
/**
* CLI connection version2 that does transport encryption.
*/
private
void
runCliConnect2
()
throws
IOException
,
InterruptedException
{
try
{
DataOutputStream
out
=
new
DataOutputStream
(
s
.
getOutputStream
());
out
.
writeUTF
(
"Welcome"
);
// perform coin-toss and come up with a session key to encrypt data
Connection
c
=
new
Connection
(
s
);
byte
[]
secret
=
c
.
diffieHellman
(
true
).
generateSecret
();
SecretKey
sessionKey
=
new
SecretKeySpec
(
Connection
.
fold
(
secret
,
128
/
8
),
"AES"
);
c
=
c
.
encryptConnection
(
sessionKey
,
"AES/CFB8/NoPadding"
);
try
{
// HACK: TODO: move the transport support into modules
Class
<?>
cls
=
Jenkins
.
getInstance
().
pluginManager
.
uberClassLoader
.
loadClass
(
"org.jenkinsci.main.modules.instance_identity.InstanceIdentity"
);
Object
iid
=
cls
.
getDeclaredMethod
(
"get"
).
invoke
(
null
);
PrivateKey
instanceId
=
(
PrivateKey
)
cls
.
getDeclaredMethod
(
"getPrivate"
).
invoke
(
iid
);
// send a signature to prove our identity
Signature
signer
=
Signature
.
getInstance
(
"SHA1withRSA"
);
signer
.
initSign
(
instanceId
);
signer
.
update
(
secret
);
c
.
writeByteArray
(
signer
.
sign
());
}
catch
(
ClassNotFoundException
e
)
{
throw
new
Error
(
e
);
}
catch
(
IllegalAccessException
e
)
{
throw
new
Error
(
e
);
}
catch
(
InvocationTargetException
e
)
{
throw
new
Error
(
e
);
}
catch
(
NoSuchMethodException
e
)
{
throw
new
Error
(
e
);
}
runCli
(
c
);
}
catch
(
GeneralSecurityException
e
)
{
throw
new
IOException2
(
"Failed to encrypt the CLI channel"
,
e
);
}
}
private
void
runCli
(
Connection
c
)
throws
IOException
,
InterruptedException
{
Channel
channel
=
new
Channel
(
"CLI channel from "
+
s
.
getInetAddress
(),
Computer
.
threadPoolForRemoting
,
Mode
.
BINARY
,
new
BufferedInputStream
(
c
.
in
),
new
BufferedOutputStream
(
c
.
out
),
null
,
true
,
Jenkins
.
getInstance
().
pluginManager
.
uberClassLoader
);
channel
.
setProperty
(
CliEntryPoint
.
class
.
getName
(),
new
CliManagerImpl
(
channel
));
channel
.
join
();
}
/**
* Handles JNLP slave agent connection request.
*/
private
void
runJnlpConnect
(
DataInputStream
in
,
PrintWriter
out
)
throws
IOException
,
InterruptedException
{
if
(!
getSecretKey
().
equals
(
in
.
readUTF
()))
{
error
(
out
,
"Unauthorized access"
);
return
;
}
final
String
nodeName
=
in
.
readUTF
();
SlaveComputer
computer
=
(
SlaveComputer
)
Jenkins
.
getInstance
().
getComputer
(
nodeName
);
if
(
computer
==
null
)
{
error
(
out
,
"No such slave: "
+
nodeName
);
return
;
}
if
(
computer
.
getChannel
()!=
null
)
{
error
(
out
,
nodeName
+
" is already connected to this master. Rejecting this connection."
);
return
;
}
out
.
println
(
Engine
.
GREETING_SUCCESS
);
jnlpConnect
(
computer
);
}
/**
* Handles JNLP slave agent connection request (v2 protocol)
*/
private
void
runJnlp2Connect
(
DataInputStream
in
,
PrintWriter
out
)
throws
IOException
,
InterruptedException
{
Properties
request
=
new
Properties
();
request
.
load
(
new
ByteArrayInputStream
(
in
.
readUTF
().
getBytes
(
"UTF-8"
)));
if
(!
getSecretKey
().
equals
(
request
.
getProperty
(
"Secret-Key"
)))
{
error
(
out
,
"Unauthorized access"
);
return
;
}
final
String
nodeName
=
request
.
getProperty
(
"Node-Name"
);
SlaveComputer
computer
=
(
SlaveComputer
)
Jenkins
.
getInstance
().
getComputer
(
nodeName
);
if
(
computer
==
null
)
{
error
(
out
,
"No such slave: "
+
nodeName
);
return
;
}
Channel
ch
=
computer
.
getChannel
();
if
(
ch
!=
null
)
{
String
c
=
request
.
getProperty
(
"Cookie"
);
if
(
c
!=
null
&&
c
.
equals
(
ch
.
getProperty
(
COOKIE_NAME
)))
{
// we think we are currently connected, but this request proves that it's from the party
// we are supposed to be communicating to. so let the current one get disconnected
LOGGER
.
info
(
"Disconnecting "
+
nodeName
+
" as we are reconnected from the current peer"
);
try
{
computer
.
disconnect
(
new
ConnectionFromCurrentPeer
()).
get
(
15
,
TimeUnit
.
SECONDS
);
}
catch
(
ExecutionException
e
)
{
throw
new
IOException2
(
"Failed to disconnect the current client"
,
e
);
}
catch
(
TimeoutException
e
)
{
throw
new
IOException2
(
"Failed to disconnect the current client"
,
e
);
}
}
else
{
error
(
out
,
nodeName
+
" is already connected to this master. Rejecting this connection."
);
return
;
}
}
out
.
println
(
Engine
.
GREETING_SUCCESS
);
Properties
response
=
new
Properties
();
String
cookie
=
generateCookie
();
response
.
put
(
"Cookie"
,
cookie
);
writeResponseHeaders
(
out
,
response
);
ch
=
jnlpConnect
(
computer
);
ch
.
setProperty
(
COOKIE_NAME
,
cookie
);
}
private
void
writeResponseHeaders
(
PrintWriter
out
,
Properties
response
)
{
for
(
Entry
<
Object
,
Object
>
e
:
response
.
entrySet
())
{
out
.
println
(
e
.
getKey
()+
": "
+
e
.
getValue
());
}
out
.
println
();
// empty line to conclude the response header
}
private
String
generateCookie
()
{
byte
[]
cookie
=
new
byte
[
32
];
new
SecureRandom
().
nextBytes
(
cookie
);
return
Util
.
toHexString
(
cookie
);
}
private
Channel
jnlpConnect
(
SlaveComputer
computer
)
throws
InterruptedException
,
IOException
{
final
String
nodeName
=
computer
.
getName
();
final
OutputStream
log
=
computer
.
openLogFile
();
PrintWriter
logw
=
new
PrintWriter
(
log
,
true
);
logw
.
println
(
"JNLP agent connected from "
+
this
.
s
.
getInetAddress
());
try
{
computer
.
setChannel
(
new
BufferedInputStream
(
this
.
s
.
getInputStream
()),
new
BufferedOutputStream
(
this
.
s
.
getOutputStream
()),
log
,
new
Listener
()
{
@Override
public
void
onClosed
(
Channel
channel
,
IOException
cause
)
{
if
(
cause
!=
null
)
LOGGER
.
log
(
Level
.
WARNING
,
"Connection #"
+
id
+
" for + "
+
nodeName
+
" terminated"
,
cause
);
try
{
ConnectionHandler
.
this
.
s
.
close
();
}
catch
(
IOException
e
)
{
// ignore
}
}
});
return
computer
.
getChannel
();
}
catch
(
AbortException
e
)
{
logw
.
println
(
e
.
getMessage
());
logw
.
println
(
"Failed to establish the connection with the slave"
);
throw
e
;
}
catch
(
IOException
e
)
{
logw
.
println
(
"Failed to establish the connection with the slave "
+
nodeName
);
e
.
printStackTrace
(
logw
);
throw
e
;
}
}
private
void
error
(
PrintWriter
out
,
String
msg
)
throws
IOException
{
out
.
println
(
msg
);
LOGGER
.
log
(
Level
.
WARNING
,
"Connection #"
+
id
+
" is aborted: "
+
msg
);
...
...
@@ -425,8 +188,6 @@ public final class TcpSlaveAgentListener extends Thread {
private
static
final
Logger
LOGGER
=
Logger
.
getLogger
(
TcpSlaveAgentListener
.
class
.
getName
());
private
static
final
String
COOKIE_NAME
=
TcpSlaveAgentListener
.
class
.
getName
()+
".cookie"
;
/**
* Host name that we advertise the CLI client to connect to.
* This is primarily for those who have reverse proxies in place such that the HTTP host name
...
...
core/src/main/java/hudson/cli/CliProtocol.java
0 → 100644
浏览文件 @
5f51089b
package
hudson.cli
;
import
hudson.Extension
;
import
hudson.model.Computer
;
import
hudson.remoting.Channel
;
import
hudson.remoting.Channel.Mode
;
import
jenkins.AgentProtocol
;
import
jenkins.model.Jenkins
;
import
java.io.BufferedInputStream
;
import
java.io.BufferedOutputStream
;
import
java.io.IOException
;
import
java.io.PrintWriter
;
import
java.net.Socket
;
/**
* {@link AgentProtocol} that accepts connection from CLI clients.
*
* @author Kohsuke Kawaguchi
* @since 1.467
*/
@Extension
public
class
CliProtocol
extends
AgentProtocol
{
@Override
public
String
getName
()
{
return
"CLI-connect"
;
}
@Override
public
void
handle
(
Socket
socket
)
throws
IOException
,
InterruptedException
{
new
Handler
(
socket
).
run
();
}
protected
static
class
Handler
{
protected
final
Socket
socket
;
public
Handler
(
Socket
socket
)
{
this
.
socket
=
socket
;
}
public
void
run
()
throws
IOException
,
InterruptedException
{
PrintWriter
out
=
new
PrintWriter
(
socket
.
getOutputStream
(),
true
);
out
.
println
(
"Welcome"
);
runCli
(
new
Connection
(
socket
));
}
protected
void
runCli
(
Connection
c
)
throws
IOException
,
InterruptedException
{
Channel
channel
=
new
Channel
(
"CLI channel from "
+
socket
.
getInetAddress
(),
Computer
.
threadPoolForRemoting
,
Mode
.
BINARY
,
new
BufferedInputStream
(
c
.
in
),
new
BufferedOutputStream
(
c
.
out
),
null
,
true
,
Jenkins
.
getInstance
().
pluginManager
.
uberClassLoader
);
channel
.
setProperty
(
CliEntryPoint
.
class
.
getName
(),
new
CliManagerImpl
(
channel
));
channel
.
join
();
}
}
}
core/src/main/java/hudson/cli/CliProtocol2.java
0 → 100644
浏览文件 @
5f51089b
package
hudson.cli
;
import
hudson.Extension
;
import
hudson.util.IOException2
;
import
jenkins.model.Jenkins
;
import
javax.crypto.SecretKey
;
import
javax.crypto.spec.SecretKeySpec
;
import
java.io.DataOutputStream
;
import
java.io.IOException
;
import
java.lang.reflect.InvocationTargetException
;
import
java.net.Socket
;
import
java.security.GeneralSecurityException
;
import
java.security.PrivateKey
;
import
java.security.Signature
;
/**
* {@link CliProtocol} Version 2, which adds transport encryption.
*
* @author Kohsuke Kawaguchi
* @since 1.467
*/
@Extension
public
class
CliProtocol2
extends
CliProtocol
{
@Override
public
String
getName
()
{
return
"CLI2-connect"
;
}
@Override
public
void
handle
(
Socket
socket
)
throws
IOException
,
InterruptedException
{
new
Handler2
(
socket
).
run
();
}
protected
static
class
Handler2
extends
Handler
{
public
Handler2
(
Socket
socket
)
{
super
(
socket
);
}
@Override
public
void
run
()
throws
IOException
,
InterruptedException
{
try
{
DataOutputStream
out
=
new
DataOutputStream
(
socket
.
getOutputStream
());
out
.
writeUTF
(
"Welcome"
);
// perform coin-toss and come up with a session key to encrypt data
Connection
c
=
new
Connection
(
socket
);
byte
[]
secret
=
c
.
diffieHellman
(
true
).
generateSecret
();
SecretKey
sessionKey
=
new
SecretKeySpec
(
Connection
.
fold
(
secret
,
128
/
8
),
"AES"
);
c
=
c
.
encryptConnection
(
sessionKey
,
"AES/CFB8/NoPadding"
);
try
{
// HACK: TODO: move the transport support into modules
Class
<?>
cls
=
Jenkins
.
getInstance
().
pluginManager
.
uberClassLoader
.
loadClass
(
"org.jenkinsci.main.modules.instance_identity.InstanceIdentity"
);
Object
iid
=
cls
.
getDeclaredMethod
(
"get"
).
invoke
(
null
);
PrivateKey
instanceId
=
(
PrivateKey
)
cls
.
getDeclaredMethod
(
"getPrivate"
).
invoke
(
iid
);
// send a signature to prove our identity
Signature
signer
=
Signature
.
getInstance
(
"SHA1withRSA"
);
signer
.
initSign
(
instanceId
);
signer
.
update
(
secret
);
c
.
writeByteArray
(
signer
.
sign
());
}
catch
(
ClassNotFoundException
e
)
{
throw
new
Error
(
e
);
}
catch
(
IllegalAccessException
e
)
{
throw
new
Error
(
e
);
}
catch
(
InvocationTargetException
e
)
{
throw
new
Error
(
e
);
}
catch
(
NoSuchMethodException
e
)
{
throw
new
Error
(
e
);
}
runCli
(
c
);
}
catch
(
GeneralSecurityException
e
)
{
throw
new
IOException2
(
"Failed to encrypt the CLI channel"
,
e
);
}
}
}
}
core/src/main/java/jenkins/AgentProtocol.java
0 → 100644
浏览文件 @
5f51089b
package
jenkins
;
import
hudson.Extension
;
import
hudson.ExtensionList
;
import
hudson.ExtensionPoint
;
import
hudson.TcpSlaveAgentListener
;
import
hudson.model.AperiodicWork
;
import
jenkins.model.Jenkins
;
import
java.io.IOException
;
import
java.net.Socket
;
/**
* Pluggable Jenkins TCP agent protocol handler called from {@link TcpSlaveAgentListener}.
*
* <p>
* To register your extension, put {@link Extension} annotation on your subtype.
* Implementations of this extension point is singleton, and its {@link #handle(Socket)} method
* gets invoked concurrently whenever a new connection comes in.
*
* @author Kohsuke Kawaguchi
* @since 1.467
* @see TcpSlaveAgentListener
*/
public
abstract
class
AgentProtocol
implements
ExtensionPoint
{
/**
* Protocol name.
*
* This is a short string that consists of printable ASCII chars. Sent by the client to select the protocol.
*/
public
abstract
String
getName
();
/**
* Called by the connection handling thread to execute the protocol.
*/
public
abstract
void
handle
(
Socket
socket
)
throws
IOException
,
InterruptedException
;
/**
* Returns all the registered {@link AperiodicWork}s.
*/
public
static
ExtensionList
<
AgentProtocol
>
all
()
{
return
Jenkins
.
getInstance
().
getExtensionList
(
AgentProtocol
.
class
);
}
public
static
AgentProtocol
of
(
String
protocolName
)
{
for
(
AgentProtocol
p
:
all
())
{
if
(
p
.
getName
().
equals
(
protocolName
))
return
p
;
}
return
null
;
}
}
core/src/main/java/jenkins/slaves/JnlpSlaveAgentProtocol.java
0 → 100644
浏览文件 @
5f51089b
package
jenkins.slaves
;
import
hudson.AbortException
;
import
hudson.Extension
;
import
hudson.remoting.Channel
;
import
hudson.remoting.Channel.Listener
;
import
hudson.remoting.Engine
;
import
hudson.slaves.SlaveComputer
;
import
jenkins.AgentProtocol
;
import
jenkins.model.Jenkins
;
import
java.io.BufferedInputStream
;
import
java.io.BufferedOutputStream
;
import
java.io.DataInputStream
;
import
java.io.DataOutputStream
;
import
java.io.IOException
;
import
java.io.OutputStream
;
import
java.io.PrintWriter
;
import
java.net.Socket
;
import
java.util.logging.Level
;
import
java.util.logging.Logger
;
/**
* {@link AgentProtocol} that accepts connection from slave agents.
*
* <h2>Security</h2>
* <p>
* Once connected, remote slave agents can send in commands to be
* executed on the master, so in a way this is like an rsh service.
* Therefore, it is important that we reject connections from
* unauthorized remote slaves.
*
* <p>
* The approach here is to have {@link Jenkins#getSecretKey() a secret key} on the master.
* This key is sent to the slave inside the <tt>.jnlp</tt> file
* (this file itself is protected by HTTP form-based authentication that
* we use everywhere else in Hudson), and the slave sends this
* token back when it connects to the master.
* Unauthorized slaves can't access the protected <tt>.jnlp</tt> file,
* so it can't impersonate a valid slave.
*
* <p>
* We don't want to force the JNLP slave agents to be restarted
* whenever the server restarts, so right now this secret master key
* is generated once and used forever, which makes this whole scheme
* less secure.
*
* @author Kohsuke Kawaguchi
* @since 1.467
*/
@Extension
public
class
JnlpSlaveAgentProtocol
extends
AgentProtocol
{
@Override
public
String
getName
()
{
return
"JNLP-connect"
;
}
@Override
public
void
handle
(
Socket
socket
)
throws
IOException
,
InterruptedException
{
new
Handler
(
socket
).
run
();
}
protected
static
class
Handler
{
protected
final
Socket
socket
;
/**
* Wrapping Socket input stream.
*/
protected
final
DataInputStream
in
;
/**
* For writing handshaking response.
*
* This is a poor design choice that we just carry forward for compatibility.
* For better protocol design, {@link DataOutputStream} is preferred for newer
* protocols.
*/
protected
final
PrintWriter
out
;
public
Handler
(
Socket
socket
)
throws
IOException
{
this
.
socket
=
socket
;
in
=
new
DataInputStream
(
socket
.
getInputStream
());
out
=
new
PrintWriter
(
socket
.
getOutputStream
(),
true
);
}
protected
void
run
()
throws
IOException
,
InterruptedException
{
if
(!
getSecretKey
().
equals
(
in
.
readUTF
()))
{
error
(
out
,
"Unauthorized access"
);
return
;
}
final
String
nodeName
=
in
.
readUTF
();
SlaveComputer
computer
=
(
SlaveComputer
)
Jenkins
.
getInstance
().
getComputer
(
nodeName
);
if
(
computer
==
null
)
{
error
(
out
,
"No such slave: "
+
nodeName
);
return
;
}
if
(
computer
.
getChannel
()!=
null
)
{
error
(
out
,
nodeName
+
" is already connected to this master. Rejecting this connection."
);
return
;
}
out
.
println
(
Engine
.
GREETING_SUCCESS
);
jnlpConnect
(
computer
);
}
protected
Channel
jnlpConnect
(
SlaveComputer
computer
)
throws
InterruptedException
,
IOException
{
final
String
nodeName
=
computer
.
getName
();
final
OutputStream
log
=
computer
.
openLogFile
();
PrintWriter
logw
=
new
PrintWriter
(
log
,
true
);
logw
.
println
(
"JNLP agent connected from "
+
socket
.
getInetAddress
());
try
{
computer
.
setChannel
(
new
BufferedInputStream
(
socket
.
getInputStream
()),
new
BufferedOutputStream
(
socket
.
getOutputStream
()),
log
,
new
Listener
()
{
@Override
public
void
onClosed
(
Channel
channel
,
IOException
cause
)
{
if
(
cause
!=
null
)
LOGGER
.
log
(
Level
.
WARNING
,
Thread
.
currentThread
().
getName
()+
" for + "
+
nodeName
+
" terminated"
,
cause
);
try
{
socket
.
close
();
}
catch
(
IOException
e
)
{
// ignore
}
}
});
return
computer
.
getChannel
();
}
catch
(
AbortException
e
)
{
logw
.
println
(
e
.
getMessage
());
logw
.
println
(
"Failed to establish the connection with the slave"
);
throw
e
;
}
catch
(
IOException
e
)
{
logw
.
println
(
"Failed to establish the connection with the slave "
+
nodeName
);
e
.
printStackTrace
(
logw
);
throw
e
;
}
}
protected
String
getSecretKey
()
{
return
Jenkins
.
getInstance
().
getSecretKey
();
}
protected
void
error
(
PrintWriter
out
,
String
msg
)
throws
IOException
{
out
.
println
(
msg
);
LOGGER
.
log
(
Level
.
WARNING
,
Thread
.
currentThread
().
getName
()+
" is aborted: "
+
msg
);
socket
.
close
();
}
}
private
static
final
Logger
LOGGER
=
Logger
.
getLogger
(
JnlpSlaveAgentProtocol
.
class
.
getName
());
}
core/src/main/java/jenkins/slaves/JnlpSlaveAgentProtocol2.java
0 → 100644
浏览文件 @
5f51089b
package
jenkins.slaves
;
import
hudson.Extension
;
import
hudson.TcpSlaveAgentListener.ConnectionFromCurrentPeer
;
import
hudson.Util
;
import
hudson.remoting.Channel
;
import
hudson.remoting.Engine
;
import
hudson.slaves.SlaveComputer
;
import
hudson.util.IOException2
;
import
jenkins.model.Jenkins
;
import
java.io.ByteArrayInputStream
;
import
java.io.IOException
;
import
java.io.PrintWriter
;
import
java.net.Socket
;
import
java.security.SecureRandom
;
import
java.util.Map.Entry
;
import
java.util.Properties
;
import
java.util.concurrent.ExecutionException
;
import
java.util.concurrent.TimeUnit
;
import
java.util.concurrent.TimeoutException
;
import
java.util.logging.Logger
;
/**
* {@link JnlpSlaveAgentProtocol} Version 2.
*
* <p>
* This protocol extends the version 1 protocol by adding a per-client cookie,
* so that we can detect a reconnection from the slave and take appropriate action,
* when the connection disappered without the master noticing.
*
* @author Kohsuke Kawaguchi
* @since 1.467
*/
@Extension
public
class
JnlpSlaveAgentProtocol2
extends
JnlpSlaveAgentProtocol
{
@Override
public
String
getName
()
{
return
"JNLP2-connect"
;
}
@Override
public
void
handle
(
Socket
socket
)
throws
IOException
,
InterruptedException
{
new
Handler2
(
socket
).
run
();
}
protected
static
class
Handler2
extends
Handler
{
public
Handler2
(
Socket
socket
)
throws
IOException
{
super
(
socket
);
}
/**
* Handles JNLP slave agent connection request (v2 protocol)
*/
@Override
protected
void
run
()
throws
IOException
,
InterruptedException
{
Properties
request
=
new
Properties
();
request
.
load
(
new
ByteArrayInputStream
(
in
.
readUTF
().
getBytes
(
"UTF-8"
)));
if
(!
getSecretKey
().
equals
(
request
.
getProperty
(
"Secret-Key"
)))
{
error
(
out
,
"Unauthorized access"
);
return
;
}
final
String
nodeName
=
request
.
getProperty
(
"Node-Name"
);
SlaveComputer
computer
=
(
SlaveComputer
)
Jenkins
.
getInstance
().
getComputer
(
nodeName
);
if
(
computer
==
null
)
{
error
(
out
,
"No such slave: "
+
nodeName
);
return
;
}
Channel
ch
=
computer
.
getChannel
();
if
(
ch
!=
null
)
{
String
c
=
request
.
getProperty
(
"Cookie"
);
if
(
c
!=
null
&&
c
.
equals
(
ch
.
getProperty
(
COOKIE_NAME
)))
{
// we think we are currently connected, but this request proves that it's from the party
// we are supposed to be communicating to. so let the current one get disconnected
LOGGER
.
info
(
"Disconnecting "
+
nodeName
+
" as we are reconnected from the current peer"
);
try
{
computer
.
disconnect
(
new
ConnectionFromCurrentPeer
()).
get
(
15
,
TimeUnit
.
SECONDS
);
}
catch
(
ExecutionException
e
)
{
throw
new
IOException2
(
"Failed to disconnect the current client"
,
e
);
}
catch
(
TimeoutException
e
)
{
throw
new
IOException2
(
"Failed to disconnect the current client"
,
e
);
}
}
else
{
error
(
out
,
nodeName
+
" is already connected to this master. Rejecting this connection."
);
return
;
}
}
out
.
println
(
Engine
.
GREETING_SUCCESS
);
Properties
response
=
new
Properties
();
String
cookie
=
generateCookie
();
response
.
put
(
"Cookie"
,
cookie
);
writeResponseHeaders
(
out
,
response
);
ch
=
jnlpConnect
(
computer
);
ch
.
setProperty
(
COOKIE_NAME
,
cookie
);
}
private
void
writeResponseHeaders
(
PrintWriter
out
,
Properties
response
)
{
for
(
Entry
<
Object
,
Object
>
e
:
response
.
entrySet
())
{
out
.
println
(
e
.
getKey
()+
": "
+
e
.
getValue
());
}
out
.
println
();
// empty line to conclude the response header
}
private
String
generateCookie
()
{
byte
[]
cookie
=
new
byte
[
32
];
new
SecureRandom
().
nextBytes
(
cookie
);
return
Util
.
toHexString
(
cookie
);
}
}
private
static
final
Logger
LOGGER
=
Logger
.
getLogger
(
JnlpSlaveAgentProtocol2
.
class
.
getName
());
private
static
final
String
COOKIE_NAME
=
JnlpSlaveAgentProtocol2
.
class
.
getName
()+
".cookie"
;
}
编辑
预览
Markdown
is supported
0%
请重试
或
添加新附件
.
添加附件
取消
You are about to add
0
people
to the discussion. Proceed with caution.
先完成此消息的编辑!
取消
想要评论请
注册
或
登录