Merge branch 'master' into security-master

This commit is contained in:
Jeff Thompson committed 2020-01-27 13:36:19 -07:00
commit c365300f99
172 files changed
+9365 -2622

No files matched your search

@@ -497,7 +497,7 @@ public abstract class ExtensionFinder implements ExtensionPoint {
m.setAccessible(true);
m.invoke(ecl, c);
}
for (Class cc = c; cc != Object.class; cc = cc.getSuperclass()) {
for (Class<?> cc = c; cc != Object.class && cc != null; cc = cc.getSuperclass()) {
/**
* See {@link com.google.inject.spi.InjectionPoint#getInjectionPoints(TypeLiteral, boolean, Errors)}
*/
+177 -98
View File
@@ -43,20 +43,26 @@ import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import hudson.Extension;
import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.io.PrintStream;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.nio.charset.UnsupportedCharsetException;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.atomic.AtomicReference;
import java.util.logging.Level;
import java.util.logging.Logger;
import jenkins.util.FullDuplexHttpService;
import jenkins.websocket.WebSocketSession;
import jenkins.websocket.WebSockets;
import org.acegisecurity.Authentication;
import org.kohsuke.stapler.HttpResponse;
import org.kohsuke.stapler.HttpResponses;
/**
@@ -100,6 +106,74 @@ public class CLIAction implements UnprotectedRootAction, StaplerProxy {
req.getView(this, "command.jelly").forward(req, rsp);
}
/** for Jelly */
public boolean isWebSocketSupported() {
return WebSockets.isSupported();
}
/**
* WebSocket endpoint.
*/
public HttpResponse doWs() {
if (!WebSockets.isSupported()) {
return HttpResponses.notFound();
}
Authentication authentication = Jenkins.getAuthentication();
return WebSockets.upgrade(new WebSocketSession() {
ServerSideImpl connection;
class OutputImpl implements PlainCLIProtocol.Output {
@Override
public void send(byte[] data) throws IOException {
sendBinary(ByteBuffer.wrap(data));
}
@Override
public void close() throws IOException {
doClose();
}
}
private void doClose() {
close();
}
@Override
protected void opened() {
try {
connection = new ServerSideImpl(new OutputImpl(), authentication);
} catch (IOException x) {
error(x);
return;
}
new Thread(() -> {
try {
try {
connection.run();
} finally {
connection.close();
}
} catch (Exception x) {
error(x);
}
}, "CLI handler for " + authentication.getName()).start();
}
@Override
protected void binary(byte[] payload, int offset, int len) {
try {
connection.handle(new DataInputStream(new ByteArrayInputStream(payload, offset, len)));
} catch (IOException x) {
error(x);
}
}
@Override
protected void error(Throwable cause) {
LOGGER.log(Level.WARNING, null, cause);
}
@Override
protected void closed(int statusCode, String reason) {
LOGGER.fine(() -> "closed: " + statusCode + ": " + reason);
connection.handleClose();
}
});
}
@Override
public Object getTarget() {
StaplerRequest req = Stapler.getCurrentRequest();
@@ -116,6 +190,105 @@ public class CLIAction implements UnprotectedRootAction, StaplerProxy {
}
}
class ServerSideImpl extends PlainCLIProtocol.ServerSide {
private Thread runningThread;
private boolean ready;
private final List<String> args = new ArrayList<>();
private Locale locale = Locale.getDefault();
private Charset encoding = Charset.defaultCharset();
private final PipedInputStream stdin = new PipedInputStream();
private final PipedOutputStream stdinMatch = new PipedOutputStream();
private final Authentication authentication;
ServerSideImpl(PlainCLIProtocol.Output out, Authentication authentication) throws IOException {
super(out);
stdinMatch.connect(stdin);
this.authentication = authentication;
}
@Override
protected void onArg(String text) {
args.add(text);
}
@Override
protected void onLocale(String text) {
for (Locale _locale : Locale.getAvailableLocales()) {
if (_locale.toString().equals(text)) {
locale = _locale;
return;
}
}
LOGGER.log(Level.WARNING, "unknown client locale {0}", text);
}
@Override
protected void onEncoding(String text) {
try {
encoding = Charset.forName(text);
} catch (UnsupportedCharsetException x) {
LOGGER.log(Level.WARNING, "unknown client charset {0}", text);
}
}
@Override
protected void onStart() {
ready();
}
@Override
protected void onStdin(byte[] chunk) throws IOException {
stdinMatch.write(chunk);
}
@Override
protected void onEndStdin() throws IOException {
stdinMatch.close();
}
@Override
protected void handleClose() {
ready();
if (runningThread != null) {
runningThread.interrupt();
}
}
private synchronized void ready() {
ready = true;
notifyAll();
}
void run() throws IOException, InterruptedException {
synchronized (this) {
while (!ready) {
wait();
}
}
PrintStream stdout = new PrintStream(streamStdout(), false, encoding.name());
PrintStream stderr = new PrintStream(streamStderr(), true, encoding.name());
if (args.isEmpty()) {
stderr.println("Connection closed before arguments received");
sendExit(2);
return;
}
String commandName = args.get(0);
CLICommand command = CLICommand.clone(commandName);
if (command == null) {
stderr.println("No such command " + commandName);
sendExit(2);
return;
}
command.setTransportAuth(authentication);
command.setClientCharset(encoding);
CLICommand orig = CLICommand.setCurrent(command);
try {
runningThread = Thread.currentThread();
int exit = command.main(args.subList(1, args.size()), locale, stdin, stdout, stderr);
stdout.flush();
sendExit(exit);
try { // seems to avoid ReadPendingException from Jetty
Thread.sleep(1000);
} catch (InterruptedException x) {
// expected; ignore
}
} finally {
CLICommand.setCurrent(orig);
runningThread = null;
}
}
}
/**
* Serves {@link PlainCLIProtocol} response.
*/
@@ -130,103 +303,9 @@ public class CLIAction implements UnprotectedRootAction, StaplerProxy {
return new FullDuplexHttpService(uuid) {
@Override
protected void run(InputStream upload, OutputStream download) throws IOException, InterruptedException {
final AtomicReference<Thread> runningThread = new AtomicReference<>();
class ServerSideImpl extends PlainCLIProtocol.ServerSide {
boolean ready;
List<String> args = new ArrayList<>();
Locale locale = Locale.getDefault();
Charset encoding = Charset.defaultCharset();
final PipedInputStream stdin = new PipedInputStream();
final PipedOutputStream stdinMatch = new PipedOutputStream();
ServerSideImpl(InputStream is, OutputStream os) throws IOException {
super(is, os);
stdinMatch.connect(stdin);
}
@Override
protected void onArg(String text) {
args.add(text);
}
@Override
protected void onLocale(String text) {
for (Locale _locale : Locale.getAvailableLocales()) {
if (_locale.toString().equals(text)) {
locale = _locale;
return;
}
}
LOGGER.log(Level.WARNING, "unknown client locale {0}", text);
}
@Override
protected void onEncoding(String text) {
try {
encoding = Charset.forName(text);
} catch (UnsupportedCharsetException x) {
LOGGER.log(Level.WARNING, "unknown client charset {0}", text);
}
}
@Override
protected void onStart() {
ready();
}
@Override
protected void onStdin(byte[] chunk) throws IOException {
stdinMatch.write(chunk);
}
@Override
protected void onEndStdin() throws IOException {
stdinMatch.close();
}
@Override
protected void handleClose() {
ready();
Thread t = runningThread.get();
if (t != null) {
t.interrupt();
}
}
private synchronized void ready() {
ready = true;
notifyAll();
}
}
try (ServerSideImpl connection = new ServerSideImpl(upload, download)) {
connection.begin();
synchronized (connection) {
while (!connection.ready) {
connection.wait();
}
}
PrintStream stdout = new PrintStream(connection.streamStdout(), false, connection.encoding.name());
PrintStream stderr = new PrintStream(connection.streamStderr(), true, connection.encoding.name());
if (connection.args.isEmpty()) {
stderr.println("Connection closed before arguments received");
connection.sendExit(2);
return;
}
String commandName = connection.args.get(0);
CLICommand command = CLICommand.clone(commandName);
if (command == null) {
stderr.println("No such command " + commandName);
connection.sendExit(2);
return;
}
command.setTransportAuth(Jenkins.getAuthentication());
command.setClientCharset(connection.encoding);
CLICommand orig = CLICommand.setCurrent(command);
try {
runningThread.set(Thread.currentThread());
int exit = command.main(connection.args.subList(1, connection.args.size()), connection.locale, connection.stdin, stdout, stderr);
stdout.flush();
connection.sendExit(exit);
try { // seems to avoid ReadPendingException from Jetty
Thread.sleep(1000);
} catch (InterruptedException x) {
// expected; ignore
}
} finally {
CLICommand.setCurrent(orig);
runningThread.set(null);
}
try (ServerSideImpl connection = new ServerSideImpl(new PlainCLIProtocol.FramedOutput(download), Jenkins.getAuthentication())) {
new PlainCLIProtocol.FramedReader(connection, upload).start();
connection.run();
}
}
};
@@ -244,8 +244,6 @@ public abstract class CLICommand implements ExtensionPoint, Cloneable {
if (!(this instanceof HelpCommand || this instanceof WhoAmICommand))
Jenkins.get().checkPermission(Jenkins.READ);
p.parseArgument(args.toArray(new String[0]));
if (!(this instanceof HelpCommand || this instanceof WhoAmICommand))
Jenkins.get().checkPermission(Jenkins.READ);
LOGGER.log(Level.FINE, "Invoking CLI command {0}, with {1} arguments, as user {2}.",
new Object[] {getName(), args.size(), auth.getName()});
int res = run();
@@ -26,9 +26,6 @@ package hudson.cli;
import hudson.AbortException;
import hudson.Extension;
import hudson.model.Computer;
import hudson.model.ComputerSet;
import hudson.util.EditDistance;
import jenkins.model.Jenkins;
import org.kohsuke.args4j.Argument;
import org.kohsuke.args4j.Option;
@@ -27,9 +27,6 @@ package hudson.cli;
import hudson.AbortException;
import hudson.Extension;
import hudson.model.Computer;
import hudson.model.ComputerSet;
import hudson.util.EditDistance;
import jenkins.model.Jenkins;
import org.kohsuke.args4j.Argument;
@@ -57,8 +57,6 @@ import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
import jenkins.model.Jenkins;
import org.acegisecurity.context.SecurityContext;
import org.acegisecurity.context.SecurityContextHolder;
import org.jenkinsci.Symbol;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
@@ -665,6 +665,7 @@ public abstract class AbstractProject<P extends AbstractProject<P,R>,R extends A
}
@Override
@Exported
public boolean isDisabled() {
return disabled;
}
@@ -24,7 +24,6 @@
package hudson.model;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import hudson.ExtensionList;
import hudson.FilePath;
import hudson.Util;
import java.io.IOException;
@@ -29,7 +29,6 @@ import hudson.Util;
import hudson.diagnosis.OldDataMonitor;
import hudson.model.Descriptor.FormException;
import hudson.model.listeners.ItemListener;
import hudson.search.CollectionSearchIndex;
import hudson.search.SearchIndexBuilder;
import hudson.security.ACL;
import hudson.security.ACLContext;
+10 -14
View File
@@ -407,23 +407,19 @@ public abstract class Slave extends Node implements Serializable {
if (!ALLOWED_JNLPJARS_FILES.contains(name)) {
throw new MalformedURLException("The specified file path " + fileName + " is not allowed due to security reasons");
}
Class<?> owner = null;
if (name.equals("hudson-cli.jar") || name.equals("jenkins-cli.jar")) {
File cliJar = Which.jarFile(CLI.class);
if (cliJar.isFile()) {
name = "jenkins-cli.jar";
} else {
URL res = findExecutableJar(cliJar, CLI.class);
if (res != null) {
return res;
}
}
owner = CLI.class;
} else if (name.equals("agent.jar") || name.equals("slave.jar") || name.equals("remoting.jar")) {
File remotingJar = Which.jarFile(hudson.remoting.Launcher.class);
if (remotingJar.isFile()) {
name = "lib/" + remotingJar.getName();
owner = hudson.remoting.Launcher.class;
}
if (owner != null) {
File jar = Which.jarFile(owner);
if (jar.isFile()) {
name = "lib/" + jar.getName();
} else {
URL res = findExecutableJar(remotingJar, hudson.remoting.Launcher.class);
URL res = findExecutableJar(jar, owner);
if (res != null) {
return res;
}
@@ -31,7 +31,6 @@ import hudson.CopyOnWrite;
import hudson.Extension;
import hudson.ExtensionList;
import hudson.ExtensionPoint;
import hudson.FeedAdapter;
import hudson.Util;
import hudson.XmlFile;
import hudson.init.InitMilestone;
@@ -33,7 +33,6 @@ import hudson.Functions;
import hudson.Indenter;
import hudson.Util;
import hudson.model.Descriptor.FormException;
import hudson.model.labels.LabelAtomPropertyDescriptor;
import hudson.model.listeners.ItemListener;
import hudson.scm.ChangeLogSet;
import hudson.scm.ChangeLogSet.Entry;
@@ -180,7 +180,7 @@ public abstract class ComputerLauncher extends AbstractDescribableImpl<ComputerL
final BufferedReader r)
throws IOException {
String line;
Pattern p = Pattern.compile("(?i)(?:java|openjdk) version \"([0-9.]+).*\"");
Pattern p = Pattern.compile("(?i)(?:java|openjdk) version \"([0-9.]+).*\".*");
while (null != (line = r.readLine())) {
Matcher m = p.matcher(line);
if (m.matches()) {
@@ -25,7 +25,6 @@ package hudson.slaves;
import hudson.model.Slave;
import hudson.model.Descriptor.FormException;
import hudson.slaves.DumbSlave;
import hudson.Extension;
import java.io.IOException;
@@ -27,19 +27,21 @@ import hudson.Extension;
import hudson.Util;
import hudson.model.Computer;
import hudson.model.Descriptor;
import hudson.model.DescriptorVisibilityFilter;
import hudson.model.TaskListener;
import hudson.util.FormValidation;
import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
import jenkins.model.Jenkins;
import jenkins.slaves.RemotingWorkDirSettings;
import jenkins.util.java.JavaUtils;
import jenkins.websocket.WebSockets;
import org.jenkinsci.Symbol;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.DataBoundSetter;
import org.kohsuke.stapler.QueryParameter;
/**
* {@link ComputerLauncher} via inbound connections.
@@ -72,6 +74,8 @@ public class JNLPLauncher extends ComputerLauncher {
@Nonnull
private RemotingWorkDirSettings workDirSettings = RemotingWorkDirSettings.getEnabledDefaults();
private boolean webSocket;
/**
* Constructor.
* @param tunnel Tunnel settings
@@ -143,6 +147,21 @@ public class JNLPLauncher extends ComputerLauncher {
return false;
}
/**
* @since TODO
*/
public boolean isWebSocket() {
return webSocket;
}
/**
* @since TODO
*/
@DataBoundSetter
public void setWebSocket(boolean webSocket) {
this.webSocket = webSocket;
}
@Override
public void launch(SlaveComputer computer, TaskListener listener) {
// do nothing as we cannot self start
@@ -196,25 +215,23 @@ public class JNLPLauncher extends ComputerLauncher {
// Causes JENKINS-45895 in the case of includes otherwise
return DescriptorImpl.class.equals(getClass());
}
}
/**
* Hides the JNLP launcher when the JNLP agent port is not enabled.
*
* @since 2.16
*/
@Extension
public static class DescriptorVisibilityFilterImpl extends DescriptorVisibilityFilter {
@Override
public boolean filter(@CheckForNull Object context, @Nonnull Descriptor descriptor) {
return descriptor.clazz != JNLPLauncher.class || Jenkins.get().getTcpSlaveAgentListener() != null;
public FormValidation doCheckWebSocket(@QueryParameter boolean webSocket, @QueryParameter String tunnel) {
if (webSocket) {
if (!WebSockets.isSupported()) {
return FormValidation.error("WebSocket support is not enabled in this Jenkins installation");
}
if (Util.fixEmptyAndTrim(tunnel) != null) {
return FormValidation.error("Tunneling is not supported in WebSocket mode");
}
} else {
if (Jenkins.get().getTcpSlaveAgentListener() == null) {
return FormValidation.error("Either WebSocket mode is selected, or the TCP port for inbound agents must be enabled");
}
}
return FormValidation.ok();
}
@Override
public boolean filterType(@Nonnull Class<?> contextClass, @Nonnull Descriptor descriptor) {
return descriptor.clazz != JNLPLauncher.class || Jenkins.get().getTcpSlaveAgentListener() != null;
}
}
/**
@@ -50,6 +50,27 @@ import jenkins.util.Timer;
*/
public abstract class SafeTimerTask extends TimerTask {
/**
* Lambda-friendly means of creating a task.
* @since TODO
*/
public static SafeTimerTask of(ExceptionRunnable r) {
return new SafeTimerTask() {
@Override
protected void doRun() throws Exception {
r.run();
}
};
}
/**
* @see #of
* @since TODO
*/
@FunctionalInterface
public interface ExceptionRunnable {
void run() throws Exception;
}
/**
* System property to change the location where (tasks) logging should be sent.
* <p><strong>Beware: changing it while Jenkins is running gives no guarantee logs will be sent to the new location
@@ -23,7 +23,6 @@
*/
package hudson.util;
import hudson.EnvVars;
import hudson.Functions;
import hudson.Launcher;
import hudson.ProxyConfiguration;
@@ -0,0 +1,191 @@
/*
* The MIT License
*
* Copyright 2019 CloudBees, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package jenkins.agents;
import com.google.common.collect.ImmutableMap;
import hudson.Extension;
import hudson.ExtensionList;
import hudson.model.Computer;
import hudson.model.InvisibleAction;
import hudson.model.UnprotectedRootAction;
import hudson.remoting.AbstractByteArrayCommandTransport;
import hudson.remoting.Capability;
import hudson.remoting.Channel;
import hudson.remoting.ChannelBuilder;
import hudson.remoting.Engine;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.ClosedChannelException;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Arrays;
import java.util.Collections;
import java.util.logging.Level;
import java.util.logging.Logger;
import jenkins.slaves.JnlpAgentReceiver;
import jenkins.slaves.RemotingVersionInfo;
import jenkins.websocket.WebSocketSession;
import jenkins.websocket.WebSockets;
import org.jenkinsci.remoting.engine.JnlpConnectionState;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
import org.kohsuke.stapler.HttpResponse;
import org.kohsuke.stapler.HttpResponses;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
@Extension
@Restricted(NoExternalUse.class)
public final class WebSocketAgents extends InvisibleAction implements UnprotectedRootAction {
private static final Logger LOGGER = Logger.getLogger(WebSocketAgents.class.getName());
@Override
public String getUrlName() {
return WebSockets.isSupported() ? "wsagents" : null;
}
public HttpResponse doIndex(StaplerRequest req, StaplerResponse rsp) throws IOException {
String agent = req.getHeader(JnlpConnectionState.CLIENT_NAME_KEY);
String secret = req.getHeader(JnlpConnectionState.SECRET_KEY);
String remoteCapabilityStr = req.getHeader(Capability.KEY);
if (agent == null || secret == null || remoteCapabilityStr == null) {
LOGGER.warning(() -> "incomplete headers: " + Collections.list(req.getHeaderNames()));
throw HttpResponses.errorWithoutStack(400, "This endpoint is only for use from agent.jar in WebSocket mode");
}
LOGGER.fine(() -> "receiving headers: " + Collections.list(req.getHeaderNames()));
if (!JnlpAgentReceiver.DATABASE.exists(agent)) {
LOGGER.warning(() -> "no such agent " + agent);
throw HttpResponses.errorWithoutStack(400, "no such agent");
}
if (!MessageDigest.isEqual(secret.getBytes(StandardCharsets.US_ASCII), JnlpAgentReceiver.DATABASE.getSecretOf(agent).getBytes(StandardCharsets.US_ASCII))) {
LOGGER.warning(() -> "incorrect secret for " + agent);
throw HttpResponses.forbidden();
}
JnlpConnectionState state = new JnlpConnectionState(null, ExtensionList.lookup(JnlpAgentReceiver.class));
state.setRemoteEndpointDescription(req.getRemoteAddr());
state.fireBeforeProperties();
LOGGER.fine(() -> "connecting " + agent);
state.fireAfterProperties(ImmutableMap.of(
// TODO or just pass all request headers?
JnlpConnectionState.CLIENT_NAME_KEY, agent,
JnlpConnectionState.SECRET_KEY, secret
));
Capability remoteCapability = Capability.fromASCII(remoteCapabilityStr);
LOGGER.fine(() -> "received " + remoteCapability);
rsp.setHeader(Capability.KEY, new Capability().toASCII());
rsp.setHeader(Engine.REMOTING_MINIMUM_VERSION_HEADER, RemotingVersionInfo.getMinimumSupportedVersion().toString());
rsp.setHeader(JnlpConnectionState.COOKIE_KEY, JnlpAgentReceiver.generateCookie()); // TODO figure out what this is for, if anything
return WebSockets.upgrade(new Session(state, agent, remoteCapability));
}
private static class Session extends WebSocketSession {
private final JnlpConnectionState state;
private final String agent;
private final Capability remoteCapability;
private AbstractByteArrayCommandTransport.ByteArrayReceiver receiver;
Session(JnlpConnectionState state, String agent, Capability remoteCapability) {
this.state = state;
this.agent = agent;
this.remoteCapability = remoteCapability;
}
@Override
protected void opened() {
Computer.threadPoolForRemoting.submit(() -> {
LOGGER.fine(() -> "setting up channel for " + agent);
state.fireBeforeChannel(new ChannelBuilder(agent, Computer.threadPoolForRemoting));
state.fireAfterChannel(state.getChannelBuilder().build(new Transport()));
LOGGER.fine(() -> "set up channel for " + agent);
return null;
});
}
@Override
protected void binary(byte[] payload, int offset, int len) {
LOGGER.finest(() -> "reading block of length " + len + " from " + agent);
if (offset == 0 && len == payload.length) {
receiver.handle(payload);
} else {
receiver.handle(Arrays.copyOfRange(payload, offset, offset + len));
}
}
@Override
protected void closed(int statusCode, String reason) {
LOGGER.finest(() -> "closed " + statusCode + " " + reason);
IOException x = new ClosedChannelException();
receiver.terminate(x);
state.fireChannelClosed(x);
state.fireAfterDisconnect();
}
@Override
protected void error(Throwable cause) {
LOGGER.log(Level.WARNING, null, cause);
}
class Transport extends AbstractByteArrayCommandTransport {
@Override
public void setup(AbstractByteArrayCommandTransport.ByteArrayReceiver bar) {
receiver = bar;
}
@Override
public void writeBlock(Channel chnl, byte[] bytes) throws IOException {
LOGGER.finest(() -> "writing block of length " + bytes.length + " to " + agent);
try {
sendBinary(ByteBuffer.wrap(bytes)).get();
} catch (Exception x) {
x.printStackTrace();
throw new IOException(x);
}
}
@Override
public Capability getRemoteCapability() throws IOException {
return remoteCapability;
}
@Override
public void closeWrite() throws IOException {
LOGGER.finest(() -> "closeWrite");
close();
}
@Override
public void closeRead() throws IOException {
LOGGER.finest(() -> "closeRead");
close();
}
}
}
}
@@ -7,7 +7,6 @@ import hudson.XmlFile;
import hudson.model.PersistentDescriptor;
import hudson.util.FormValidation;
import hudson.util.XStream2;
import jenkins.security.ApiTokenProperty;
import jenkins.util.SystemProperties;
import jenkins.util.UrlHelper;
import org.jenkinsci.Symbol;
@@ -1,14 +1,9 @@
package jenkins.mvn;
import hudson.EnvVars;
import hudson.Extension;
import hudson.FilePath;
import hudson.Util;
import hudson.model.AbstractBuild;
import hudson.model.TaskListener;
import hudson.util.IOUtils;
import java.io.File;
import org.apache.commons.lang.StringUtils;
import org.kohsuke.stapler.DataBoundConstructor;
@@ -1,14 +1,9 @@
package jenkins.mvn;
import hudson.EnvVars;
import hudson.Extension;
import hudson.FilePath;
import hudson.Util;
import hudson.model.AbstractBuild;
import hudson.model.TaskListener;
import hudson.util.IOUtils;
import java.io.File;
import org.apache.commons.lang.StringUtils;
import org.jenkinsci.Symbol;
@@ -115,7 +115,7 @@ public class DefaultJnlpSlaveReceiver extends JnlpAgentReceiver {
+ "Set system property "
+ "jenkins.slaves.DefaultJnlpSlaveReceiver.disableStrictVerification=true to allow"
+ "connections until the plugin has been fixed.",
new Object[]{clientName, event.getSocket().getRemoteSocketAddress(), computer.getLauncher().getClass()});
new Object[]{clientName, event.getRemoteEndpointDescription(), computer.getLauncher().getClass()});
event.reject(new ConnectionRefusalException(String.format("%s is not an inbound agent", clientName)));
return;
}
@@ -151,7 +151,7 @@ public class DefaultJnlpSlaveReceiver extends JnlpAgentReceiver {
final OutputStream log = computer.openLogFile();
state.setLog(log);
PrintWriter logw = new PrintWriter(log, true);
logw.println("Inbound agent connected from " + event.getSocket().getInetAddress());
logw.println("Inbound agent connected from " + event.getRemoteEndpointDescription());
for (ChannelConfigurator cc : ChannelConfigurator.all()) {
cc.onChannelBuilding(event.getChannelBuilder(), computer);
}
@@ -7,12 +7,13 @@ import hudson.model.Slave;
import java.security.SecureRandom;
import javax.annotation.Nonnull;
import jenkins.agents.WebSocketAgents;
import jenkins.security.HMACConfidentialKey;
import org.jenkinsci.remoting.engine.JnlpClientDatabase;
import org.jenkinsci.remoting.engine.JnlpConnectionStateListener;
/**
* Receives incoming agents connecting through {@link JnlpSlaveAgentProtocol4}.
* Receives incoming agents connecting through the likes of {@link JnlpSlaveAgentProtocol4} or {@link WebSocketAgents}.
*
* <p>
* This is useful to establish the communication with other JVMs and use them
@@ -104,7 +104,9 @@ public class MissingClassTelemetry extends Telemetry {
{"java.util.ResourceBundle$Control", "newBundle"},
//hundreds when a job is created
{"org.codehaus.groovy.control.ClassNodeResolver", "tryAsLoaderClassOrScript"},
{"org.kohsuke.stapler.RequestImpl$TypePair", "convertJSON"}
{"org.kohsuke.stapler.RequestImpl$TypePair", "convertJSON"},
{"net.bull.javamelody.FilterContext", "isMojarraAvailable"} // JENKINS-60725
};
@Nonnull
@@ -49,7 +49,6 @@ import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.StringTokenizer;
@@ -1,7 +1,5 @@
package jenkins.util;
import org.apache.tools.ant.Project;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
@@ -0,0 +1,67 @@
/*
* The MIT License
*
* Copyright 2019 CloudBees, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package jenkins.websocket;
import hudson.Extension;
import hudson.model.InvisibleAction;
import hudson.model.RootAction;
import java.nio.ByteBuffer;
import jenkins.model.Jenkins;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
import org.kohsuke.stapler.HttpResponse;
@Extension
@Restricted(NoExternalUse.class)
public class WebSocketEcho extends InvisibleAction implements RootAction {
@Override
public String getUrlName() {
return "wsecho";
}
public HttpResponse doIndex() {
Jenkins.get().checkPermission(Jenkins.ADMINISTER);
return WebSockets.upgrade(new WebSocketSession() {
@Override
protected void text(String message) {
sendText("hello " + message);
}
@Override
protected void binary(byte[] payload, int offset, int len) {
ByteBuffer data = ByteBuffer.allocate(len);
for (int i = 0; i < len; i++) {
byte b = payload[offset + i];
if (b >= 'a' && b <= 'z') {
b += 'A' - 'a';
}
data.put(i, b);
}
sendBinary(data);
}
});
}
}
@@ -0,0 +1,150 @@
/*
* The MIT License
*
* Copyright 2019 CloudBees, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package jenkins.websocket;
import java.lang.reflect.Method;
import java.nio.ByteBuffer;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import jenkins.util.SystemProperties;
import jenkins.util.Timer;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.Beta;
/**
* One WebSocket connection.
* @see WebSockets
* @since TODO
*/
@Restricted(Beta.class)
public abstract class WebSocketSession {
/**
* Number of seconds between server-sent pings.
* Zero to disable.
* <p><a href="http://nginx.org/en/docs/http/websocket.html">nginx docs</a> claim 60s timeout and this seems to match experiments.
* <a href="https://cloud.google.com/kubernetes-engine/docs/concepts/ingress#support_for_websocket">GKE docs</a> says 30s
* but this is a total timeout, not inactivity, so you need to set {@code BackendConfigSpec.timeoutSec} anyway.
* <p>This is set for the whole Jenkins session rather than a particular service,
* since it has more to do with the environment than anything else.
* Certain services may have their own “keep alive” semantics,
* but for example {@link hudson.remoting.PingThread} may be too infrequent.
*/
private static long PING_INTERVAL_SECONDS = SystemProperties.getLong("jenkins.websocket.pingInterval", 30L);
private static final Logger LOGGER = Logger.getLogger(WebSocketSession.class.getName());
private Object session;
private Object remoteEndpoint;
private ScheduledFuture<?> pings;
protected WebSocketSession() {}
Object onWebSocketSomething(Object proxy, Method method, Object[] args) throws Exception {
switch (method.getName()) {
case "onWebSocketConnect":
this.session = args[0];
this.remoteEndpoint = session.getClass().getMethod("getRemote").invoke(args[0]);
if (PING_INTERVAL_SECONDS != 0) {
pings = Timer.get().scheduleAtFixedRate(() -> {
try {
remoteEndpoint.getClass().getMethod("sendPing", ByteBuffer.class).invoke(remoteEndpoint, ByteBuffer.wrap(new byte[0]));
} catch (Exception x) {
error(x);
pings.cancel(true);
}
}, PING_INTERVAL_SECONDS / 2, PING_INTERVAL_SECONDS, TimeUnit.SECONDS);
}
opened();
return null;
case "onWebSocketClose":
if (pings != null) {
pings.cancel(true);
// alternately, check Session.isOpen each time
}
closed((Integer) args[0], (String) args[1]);
return null;
case "onWebSocketError":
error((Throwable) args[0]);
return null;
case "onWebSocketBinary":
binary((byte[]) args[0], (Integer) args[1], (Integer) args[2]);
return null;
case "onWebSocketText":
text((String) args[0]);
return null;
default:
throw new AssertionError();
}
}
protected void opened() {
}
protected void closed(int statusCode, String reason) {
}
protected void error(Throwable cause) {
LOGGER.log(Level.WARNING, "unhandled WebSocket service error", cause);
}
protected void binary(byte[] payload, int offset, int len) {
LOGGER.warning("unexpected binary frame");
}
protected void text(String message) {
LOGGER.warning("unexpected text frame");
}
@SuppressWarnings("unchecked")
protected final Future<Void> sendBinary(ByteBuffer data) {
try {
return (Future<Void>) remoteEndpoint.getClass().getMethod("sendBytesByFuture", ByteBuffer.class).invoke(remoteEndpoint, data);
} catch (Exception x) {
throw new RuntimeException(x);
}
}
@SuppressWarnings("unchecked")
protected final Future<Void> sendText(String text) {
try {
return (Future<Void>) remoteEndpoint.getClass().getMethod("sendStringByFuture", String.class).invoke(remoteEndpoint, text);
} catch (Exception x) {
throw new RuntimeException(x);
}
}
protected final void close() {
try {
session.getClass().getMethod("close").invoke(session);
} catch (Exception x) {
throw new RuntimeException(x);
}
}
}
@@ -0,0 +1,117 @@
/*
* The MIT License
*
* Copyright 2019 CloudBees, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package jenkins.websocket;
import hudson.Extension;
import hudson.ExtensionList;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.Beta;
import org.kohsuke.stapler.HttpResponse;
import org.kohsuke.stapler.HttpResponses;
import org.kohsuke.stapler.Stapler;
/**
* Support for serving WebSocket responses.
* @since TODO
*/
@Restricted(Beta.class)
@Extension
public class WebSockets {
private static final Logger LOGGER = Logger.getLogger(WebSockets.class.getName());
private static final String ATTR_SESSION = WebSockets.class.getName() + ".session";
// TODO ability to handle subprotocols?
public static HttpResponse upgrade(WebSocketSession session) {
return (req, rsp, node) -> {
try {
Object factory = ExtensionList.lookupSingleton(WebSockets.class).init();
if (!((Boolean) webSocketServletFactoryClass.getMethod("isUpgradeRequest", HttpServletRequest.class, HttpServletResponse.class).invoke(factory, req, rsp))) {
throw HttpResponses.errorWithoutStack(HttpServletResponse.SC_BAD_REQUEST, "only WS connections accepted here");
}
req.setAttribute(ATTR_SESSION, session);
if (!((Boolean) webSocketServletFactoryClass.getMethod("acceptWebSocket", HttpServletRequest.class, HttpServletResponse.class).invoke(factory, req, rsp))) {
throw HttpResponses.errorWithoutStack(HttpServletResponse.SC_BAD_REQUEST, "did not manage to upgrade");
}
} catch (HttpResponses.HttpResponseException x) {
throw x;
} catch (Exception x) {
LOGGER.log(Level.WARNING, null, x);
throw HttpResponses.error(x);
}
// OK!
};
}
private static ClassLoader cl;
private static Class<?> webSocketServletFactoryClass;
private static synchronized void staticInit() throws Exception {
if (webSocketServletFactoryClass == null) {
cl = ServletContext.class.getClassLoader();
webSocketServletFactoryClass = cl.loadClass("org.eclipse.jetty.websocket.servlet.WebSocketServletFactory");
}
}
public static boolean isSupported() {
try {
staticInit();
return true;
} catch (Exception x) {
LOGGER.log(Level.FINE, null, x);
return false;
}
}
private /*WebSocketServletFactory*/Object factory;
private synchronized Object init() throws Exception {
if (factory == null) {
staticInit();
Class<?> webSocketPolicyClass = cl.loadClass("org.eclipse.jetty.websocket.api.WebSocketPolicy");
factory = cl.loadClass("org.eclipse.jetty.websocket.servlet.WebSocketServletFactory$Loader").getMethod("load", ServletContext.class, webSocketPolicyClass).invoke(null, Stapler.getCurrent().getServletContext(), webSocketPolicyClass.getMethod("newServerPolicy").invoke(null));
webSocketServletFactoryClass.getMethod("start").invoke(factory);
Class<?> webSocketCreatorClass = cl.loadClass("org.eclipse.jetty.websocket.servlet.WebSocketCreator");
webSocketServletFactoryClass.getMethod("setCreator", webSocketCreatorClass).invoke(factory, Proxy.newProxyInstance(cl, new Class<?>[] {webSocketCreatorClass}, this::createWebSocket));
}
return factory;
}
private Object createWebSocket(Object proxy, Method method, Object[] args) throws Exception {
Object servletUpgradeRequest = args[0];
WebSocketSession session = (WebSocketSession) servletUpgradeRequest.getClass().getMethod("getServletAttribute", String.class).invoke(servletUpgradeRequest, ATTR_SESSION);
return Proxy.newProxyInstance(cl, new Class<?>[] {cl.loadClass("org.eclipse.jetty.websocket.api.WebSocketListener")}, session::onWebSocketSomething);
}
}