CLIAction.java 10.9 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
/*
 * The MIT License
 *
 * Copyright (c) 2013 Red Hat, 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 hudson.cli;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletResponse;

34
import hudson.model.UnprotectedRootAction;
35 36
import jenkins.model.Jenkins;

K
Kohsuke Kawaguchi 已提交
37
import org.jenkinsci.Symbol;
38 39
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
K
Kohsuke Kawaguchi 已提交
40 41
import org.kohsuke.stapler.Stapler;
import org.kohsuke.stapler.StaplerProxy;
42 43 44 45 46 47
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;

import hudson.Extension;
import hudson.model.FullDuplexHttpChannel;
import hudson.remoting.Channel;
48 49 50 51 52 53 54 55 56 57
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.io.PrintStream;
import java.nio.charset.Charset;
import java.nio.charset.UnsupportedCharsetException;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
58
import java.util.concurrent.atomic.AtomicReference;
59 60 61
import java.util.logging.Level;
import java.util.logging.Logger;
import jenkins.util.FullDuplexHttpService;
62
import org.kohsuke.stapler.HttpResponses;
63 64

/**
K
Kohsuke Kawaguchi 已提交
65 66
 * Shows usage of CLI and commands.
 *
67 68
 * @author ogondza
 */
K
Kohsuke Kawaguchi 已提交
69
@Extension @Symbol("cli")
70
@Restricted(NoExternalUse.class)
71
public class CLIAction implements UnprotectedRootAction, StaplerProxy {
72

73 74 75
    private static final Logger LOGGER = Logger.getLogger(CLIAction.class.getName());

    private transient final Map<UUID, FullDuplexHttpService> duplexServices = new HashMap<>();
76 77 78 79 80 81 82 83 84 85

    public String getIconFileName() {
        return null;
    }

    public String getDisplayName() {
        return "Jenkins CLI";
    }

    public String getUrlName() {
86
        return "cli";
87 88 89
    }

    public void doCommand(StaplerRequest req, StaplerResponse rsp) throws ServletException, IOException {
90
        final Jenkins jenkins = Jenkins.getActiveInstance();
91 92 93 94 95 96
        jenkins.checkPermission(Jenkins.READ);

        // Strip trailing slash
        final String commandName = req.getRestOfPath().substring(1);
        CLICommand command = CLICommand.clone(commandName);
        if (command == null) {
97
            rsp.sendError(HttpServletResponse.SC_NOT_FOUND, "No such command");
98 99 100 101 102 103 104
            return;
        }

        req.setAttribute("command", command);
        req.getView(this, "command.jelly").forward(req, rsp);
    }

K
Kohsuke Kawaguchi 已提交
105 106 107 108 109
    @Override
    public Object getTarget() {
        StaplerRequest req = Stapler.getCurrentRequest();
        if (req.getRestOfPath().length()==0 && "POST".equals(req.getMethod())) {
            // CLI connection request
110 111 112 113 114 115 116
            if ("false".equals(req.getParameter("remoting"))) {
                throw new PlainCliEndpointResponse();
            } else if (jenkins.CLI.get().isEnabled()) {
                throw new RemotingCliEndpointResponse();
            } else {
                throw HttpResponses.forbidden();
            }
K
Kohsuke Kawaguchi 已提交
117 118
        } else {
            return this;
119
        }
K
Kohsuke Kawaguchi 已提交
120
    }
121

K
Kohsuke Kawaguchi 已提交
122
    /**
123
     * Serves {@link PlainCLIProtocol} response.
K
Kohsuke Kawaguchi 已提交
124
     */
125
    private class PlainCliEndpointResponse extends FullDuplexHttpService.Response {
126

127
        PlainCliEndpointResponse() {
128 129 130
            super(duplexServices);
        }

K
Kohsuke Kawaguchi 已提交
131
        @Override
132
        protected FullDuplexHttpService createService(StaplerRequest req, UUID uuid) throws IOException {
133 134 135
            return new FullDuplexHttpService(uuid) {
                @Override
                protected void run(InputStream upload, OutputStream download) throws IOException, InterruptedException {
136
                    final AtomicReference<Thread> runningThread = new AtomicReference<>();
137
                    class ServerSideImpl extends PlainCLIProtocol.ServerSide {
138
                        boolean ready;
139 140 141 142 143 144 145 146 147 148 149 150 151 152 153
                        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) {
J
Jesse Glick 已提交
154 155 156 157 158 159 160
                            for (Locale _locale : Locale.getAvailableLocales()) {
                                if (_locale.toString().equals(text)) {
                                    locale = _locale;
                                    return;
                                }
                            }
                            LOGGER.log(Level.WARNING, "unknown client locale {0}", text);
161 162 163 164 165 166 167
                        }
                        @Override
                        protected void onEncoding(String text) {
                            try {
                                encoding = Charset.forName(text);
                            } catch (UnsupportedCharsetException x) {
                                LOGGER.log(Level.WARNING, "unknown client charset {0}", text);
168
                            }
K
Kohsuke Kawaguchi 已提交
169
                        }
170
                        @Override
171 172
                        protected void onStart() {
                            ready();
173
                        }
174 175 176
                        @Override
                        protected void onStdin(byte[] chunk) throws IOException {
                            stdinMatch.write(chunk);
177
                        }
178 179 180
                        @Override
                        protected void onEndStdin() throws IOException {
                            stdinMatch.close();
181
                        }
182
                        @Override
183 184
                        protected void handleClose() {
                            ready();
185 186 187 188 189
                            Thread t = runningThread.get();
                            if (t != null) {
                                t.interrupt();
                            }
                        }
190 191 192 193
                        private synchronized void ready() {
                            ready = true;
                            notifyAll();
                        }
194
                    }
195 196 197
                    try (ServerSideImpl connection = new ServerSideImpl(upload, download)) {
                        connection.begin();
                        synchronized (connection) {
198 199 200
                            while (!connection.ready) {
                                connection.wait();
                            }
201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223
                        }
                        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);
224 225 226 227 228
                            try { // seems to avoid ReadPendingException from Jetty
                                Thread.sleep(1000);
                            } catch (InterruptedException x) {
                                // expected; ignore
                            }
229 230 231 232
                        } finally {
                            CLICommand.setCurrent(orig);
                            runningThread.set(null);
                        }
233 234 235
                    }
                }
            };
236 237
        }
    }
238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264

    /**
     * Serves Remoting-over-HTTP response.
     */
    private class RemotingCliEndpointResponse extends FullDuplexHttpService.Response {

        RemotingCliEndpointResponse() {
            super(duplexServices);
        }

        @Override
        protected FullDuplexHttpService createService(StaplerRequest req, UUID uuid) throws IOException {
            // do not require any permission to establish a CLI connection
            // the actual authentication for the connecting Channel is done by CLICommand

            return new FullDuplexHttpChannel(uuid, !Jenkins.getInstance().hasPermission(Jenkins.ADMINISTER)) {
                @SuppressWarnings("deprecation")
                @Override
                protected void main(Channel channel) throws IOException, InterruptedException {
                    // capture the identity given by the transport, since this can be useful for SecurityRealm.createCliAuthenticator()
                    channel.setProperty(CLICommand.TRANSPORT_AUTHENTICATION, Jenkins.getAuthentication());
                    channel.setProperty(CliEntryPoint.class.getName(), new CliManagerImpl(channel));
                }
            };
        }
    }

265
}