mirror of
https://github.com/jenkinsci/jenkins.git
synced 2026-11-14 04:22:16 +00:00
Merge branch 'master' into security-master
This commit is contained in:
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)}
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in new issue
Block a user