KickJava   Java API By Example, From Geeks To Geeks.

Java > Open Source Codes > hudson > TcpSlaveAgentListener


1 package hudson;
2
3 import hudson.model.Computer;
4 import hudson.model.Hudson;
5 import hudson.model.Slave.ComputerImpl;
6 import hudson.remoting.Channel;
7 import hudson.remoting.Channel.Listener;
8 import hudson.util.TextFile;
9
10 import java.io.DataInputStream JavaDoc;
11 import java.io.IOException JavaDoc;
12 import java.io.PrintWriter JavaDoc;
13 import java.io.File JavaDoc;
14 import java.net.ServerSocket JavaDoc;
15 import java.net.Socket JavaDoc;
16 import java.util.logging.Level JavaDoc;
17 import java.util.logging.Logger JavaDoc;
18 import java.security.SecureRandom JavaDoc;
19
20 /**
21  * Listens to incoming TCP connections from JNLP slave agents.
22  *
23  * <h2>Security</h2>
24  * <p>
25  * Once connected, remote slave agents can send in commands to be
26  * executed on the master, so in a way this is like an rsh service.
27  * Therefore, it is important that we reject connections from
28  * unauthorized remote slaves.
29  *
30  * <p>
31  * The approach here is to have {@link #secretKey a secret key} on the master.
32  * This key is sent to the slave inside the <tt>.jnlp</tt> file
33  * (this file itself is protected by HTTP form-based authentication that
34  * we use everywhere else in Hudson), and the slave sends this
35  * token back when it connects to the master.
36  * Unauthorized slaves can't access the protected <tt>.jnlp</tt> file,
37  * so it can't impersonate a valid slave.
38  *
39  * <p>
40  * We don't want to force the JNLP slave agents to be restarted
41  * whenever the server restarts, so right now this secret master key
42  * is generated once and used forever, which makes this whole scheme
43  * less secure.
44  *
45  * @author Kohsuke Kawaguchi
46  */

47 public class TcpSlaveAgentListener extends Thread JavaDoc {
48
49     private final ServerSocket JavaDoc serverSocket;
50     private volatile boolean shuttingDown;
51     private final String JavaDoc secretKey;
52
53     public TcpSlaveAgentListener() throws IOException JavaDoc {
54         serverSocket = new ServerSocket JavaDoc(0);
55
56         LOGGER.info("JNLP slave agent listener started on TCP port "+getPort());
57
58         // get or create the secret
59
TextFile secretFile = new TextFile(new File(Hudson.getInstance().getRootDir(),"secret.key"));
60         if(secretFile.exists()) {
61             secretKey = secretFile.readTrim();
62         } else {
63             SecureRandom JavaDoc sr = new SecureRandom JavaDoc();
64             byte[] random = new byte[32];
65             sr.nextBytes(random);
66             secretKey = Util.toHexString(random);
67             secretFile.write(secretKey);
68         }
69
70         start();
71     }
72
73     /**
74      * Gets the TCP port number in which we are listening.
75      */

76     public int getPort() {
77         return serverSocket.getLocalPort();
78     }
79
80     public String JavaDoc getSecretKey() {
81         return secretKey;
82     }
83
84     public void run() {
85         try {
86
87             // the loop eventually terminates when the socket is closed.
88
while (true) {
89                 Socket JavaDoc s = serverSocket.accept();
90                 new ConnectionHandler(s).start();
91             }
92         } catch (IOException JavaDoc e) {
93             if(!shuttingDown) {
94                 LOGGER.log(Level.SEVERE,"Failed to accept JNLP slave agent connections",e);
95             }
96         }
97     }
98
99     /**
100      * Initiates the shuts down of the listener.
101      */

102     public void shutdown() {
103         shuttingDown = true;
104         try {
105             serverSocket.close();
106         } catch (IOException JavaDoc e) {
107             LOGGER.log(Level.WARNING, "Failed to close down TCP port",e);
108         }
109     }
110
111     private final class ConnectionHandler extends Thread JavaDoc {
112         private final Socket JavaDoc s;
113         /**
114          * Unique number to identify this connection. Used in the log.
115          */

116         private final int id;
117
118         public ConnectionHandler(Socket JavaDoc s) {
119             this.s = s;
120             synchronized(getClass()) {
121                 id = iotaGen++;
122             }
123         }
124
125         public void run() {
126             try {
127                 LOGGER.info("Accepted connection #"+id+" from "+s.getRemoteSocketAddress());
128
129                 DataInputStream JavaDoc in = new DataInputStream JavaDoc(s.getInputStream());
130                 PrintWriter JavaDoc out = new PrintWriter JavaDoc(s.getOutputStream(),true);
131
132                 if(!secretKey.equals(in.readUTF())) {
133                     error(out, "Unauthorized access");
134                     return;
135                 }
136
137                 String JavaDoc nodeName = in.readUTF();
138                 Computer computer = Hudson.getInstance().getComputer(nodeName);
139                 if(computer==null) {
140                     error(out, "No such slave: "+nodeName);
141                     return;
142                 }
143
144                 if(computer.getChannel()!=null) {
145                     error(out, "Already connected");
146                     return;
147                 }
148
149                 out.println("Welcome");
150
151                 ((ComputerImpl)computer).setChannel(s.getInputStream(),s.getOutputStream(),null,
152                     new Listener() {
153                         public void onClosed(Channel channel, IOException JavaDoc cause) {
154                             if(cause!=null)
155                                 LOGGER.log(Level.WARNING, "Connection #"+id+" terminated",cause);
156                             try {
157                                 s.close();
158                             } catch (IOException JavaDoc e) {
159                                 // ignore
160
}
161                         }
162                     });
163             } catch (InterruptedException JavaDoc e) {
164                 LOGGER.log(Level.WARNING,"Connection #"+id+" aborted",e);
165                 try {
166                     s.close();
167                 } catch (IOException JavaDoc _) {
168                     // try to clean up the socket
169
}
170             } catch (IOException JavaDoc e) {
171                 LOGGER.log(Level.WARNING,"Connection #"+id+" failed",e);
172                 try {
173                     s.close();
174                 } catch (IOException JavaDoc _) {
175                     // try to clean up the socket
176
}
177             }
178         }
179
180         private void error(PrintWriter JavaDoc out, String JavaDoc msg) throws IOException JavaDoc {
181             out.println(msg);
182             LOGGER.log(Level.WARNING,"Connection #"+id+" is aborted: "+msg);
183             s.close();
184         }
185     }
186
187     private static int iotaGen=1;
188
189     private static final Logger JavaDoc LOGGER = Logger.getLogger(TcpSlaveAgentListener.class.getName());
190 }
191
192 /*
193 Pasted from http://today.java.net/pub/a/today/2005/09/01/webstart.html
194
195     Is it unrealistic to try to control access to JWS files?
196     Is anyone doing this?
197
198 It is not unrealistic, and we are doing it. Create a protected web page
199 with a download button or link that makes a servlet call. If the user has
200 already logged in to your website, of course they can go there without
201 further authentication. The servlet reads the cookies sent by the browser
202 when the link is activated. It then generates a dynamic JNLP file adding
203 the authentication cookie and any other required cookies (JSESSIONID, etc.)
204 via <argument> tags. Write the WebStart application so that it picks up
205 any required cookies from the argument list, and adds these cookies to its
206 request headers on subsequent calls to the server. (Note: in the dynamic
207 JNLP file, do NOT put HREF= in the opening jnlp tag. If you do, JWS will
208 try to reload the JNLP from disk and since it's dynamic, it won't be there.
209 Leave it off and JWS will be happy.)
210
211 When returning the dynamic JNLP, the servlet should invoke setHeader(
212 "Expires", 0 ) and addDateHeader() twice on the servlet response to set
213 both "Date" and "Last-Modified" to the current date. This keeps the browser
214 from using a cached copy of a prior dynamic JNLP obtained from the same URL.
215
216 Note also that the JAR file(s) for the JWS application should not be on
217 a password-protected path - the launcher won't know about the authentication
218 cookie. But once the application starts, you can run all its requests
219 through a protected path requiring the authentication cookie, because
220 the application gets it from the dynamic JNLP. Just write it so that it
221 can't do anything useful without going through a protected path or doing
222 something to present credentials that could only have come from a valid
223 user.
224 */
Popular Tags