security — sub server on custom port must not provide all IDE services if it is not desired (IdeTalk XML-RPC server must provide only IdeTalk services)

This commit is contained in:
Vladimir Krivosheev
2013-07-26 12:15:26 +02:00
parent f6cadcf23e
commit 07a7a76ef9
8 changed files with 148 additions and 159 deletions
@@ -16,6 +16,13 @@
package com.intellij.ide;
import com.intellij.openapi.components.ServiceManager;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.handler.codec.http.HttpRequest;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.util.Map;
public interface XmlRpcServer {
void addHandler(String name, Object handler);
@@ -24,6 +31,8 @@ public interface XmlRpcServer {
void removeHandler(String name);
boolean process(@NotNull String path, @NotNull HttpRequest request, @NotNull ChannelHandlerContext context, @Nullable Map<String, Object> handlers) throws IOException;
final class SERVICE {
private SERVICE() {
}
@@ -18,6 +18,8 @@ package org.jetbrains.ide;
import com.intellij.openapi.extensions.ExtensionPointName;
import org.jetbrains.annotations.Nullable;
import java.util.Map;
public abstract class CustomPortServerManager {
public static final ExtensionPointName<CustomPortServerManager> EP_NAME = ExtensionPointName.create("com.intellij.customPortServerManager");
@@ -34,4 +36,11 @@ public abstract class CustomPortServerManager {
public abstract boolean isAvailableExternally();
public abstract void setManager(@Nullable CustomPortService manager);
/**
* This server will accept only XML-RPC requests if this method returns not-null map of XMl-RPC handlers
*/
public Map<String, Object> createXmlRpcHandlers() {
return null;
}
}
@@ -17,6 +17,7 @@ package com.intellij.ide;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.extensions.Extensions;
import com.intellij.openapi.vfs.CharsetToolkit;
import gnu.trove.THashMap;
import org.apache.xmlrpc.*;
import org.jboss.netty.buffer.ChannelBuffer;
@@ -33,47 +34,23 @@ import org.jetbrains.ide.HttpRequestHandler;
import org.jetbrains.io.Responses;
import java.io.IOException;
import java.util.Arrays;
import java.util.Map;
public class XmlRpcServerImpl implements XmlRpcServer {
private static final Logger LOG = Logger.getInstance(XmlRpcServerImpl.class);
private final XmlRpcHandlerMappingImpl handlerMapping;
// idea doesn't use authentication
private final XmlRpcContext xmlRpcContext = new XmlRpcContext() {
@Nullable
@Override
public String getUserName() {
return null;
}
@Nullable
@Override
public String getPassword() {
return null;
}
@Override
public XmlRpcHandlerMapping getHandlerMapping() {
return handlerMapping;
}
};
private final Map<String, Object> handlerMapping;
public XmlRpcServerImpl() {
handlerMapping = LOG.isDebugEnabled() ? new LoggingDefaultHandlerMapping() : new XmlRpcHandlerMappingImpl();
handlerMapping = new THashMap<String, Object>();
for (XmlRpcHandlerBean handlerBean : Extensions.getExtensions(XmlRpcHandlerBean.EP_NAME)) {
final Object handler;
try {
handler = handlerBean.instantiate();
handlerMapping.put(handlerBean.name, handlerBean.instantiate());
}
catch (ClassNotFoundException e) {
LOG.error(e);
continue;
}
handlerMapping.addHandler(handlerBean.name, handler);
}
LOG.debug("XmlRpcServerImpl instantiated, handlers " + handlerMapping);
}
@@ -85,27 +62,28 @@ public class XmlRpcServerImpl implements XmlRpcServer {
@Override
public boolean process(QueryStringDecoder urlDecoder, HttpRequest request, ChannelHandlerContext context) throws IOException {
return ((XmlRpcServerImpl)SERVICE.getInstance()).process(urlDecoder, request, context);
return SERVICE.getInstance().process(urlDecoder.getPath(), request, context, null);
}
}
@Override
public boolean hasHandler(String name) {
return handlerMapping.handlers.containsKey(name);
return handlerMapping.containsKey(name);
}
@Override
public void addHandler(String name, Object handler) {
handlerMapping.addHandler(name, handler);
handlerMapping.put(name, handler);
}
@Override
public void removeHandler(String name) {
handlerMapping.removeHandler(name);
handlerMapping.remove(name);
}
private boolean process(QueryStringDecoder urlDecoder, HttpRequest request, ChannelHandlerContext context) throws IOException {
if (!isXmlRpcRequest(urlDecoder.getPath())) {
@Override
public boolean process(@NotNull String path, @NotNull HttpRequest request, @NotNull ChannelHandlerContext context, @Nullable Map<String, Object> handlers) throws IOException {
if (!(path.isEmpty() || (path.length() == 1 && path.charAt(0) == '/') || path.equalsIgnoreCase("/RPC2"))) {
return false;
}
@@ -113,11 +91,13 @@ public class XmlRpcServerImpl implements XmlRpcServer {
ChannelBuffer result;
ChannelBufferInputStream in = new ChannelBufferInputStream(request.getContent());
try {
result = ChannelBuffers.copiedBuffer(new XmlRpcWorker(handlerMapping).execute(in, xmlRpcContext));
XmlRpcServerRequest xmlRpcServerRequest = new XmlRpcRequestProcessor().decodeRequest(in);
Object response = invokeHandler(getHandler(xmlRpcServerRequest.getMethodName(), handlers == null ? handlerMapping : handlers), xmlRpcServerRequest);
result = ChannelBuffers.copiedBuffer(new XmlRpcResponseProcessor().encodeResponse(response, CharsetToolkit.UTF8));
}
catch (Throwable ex) {
catch (Throwable e) {
context.getChannel().close();
LOG.error(ex);
LOG.error(e);
return true;
}
finally {
@@ -137,80 +117,32 @@ public class XmlRpcServerImpl implements XmlRpcServer {
return false;
}
private static boolean isXmlRpcRequest(String path) {
return path.isEmpty() || (path.length() == 1 && path.charAt(0) == '/') || path.equalsIgnoreCase("/RPC2");
private static Object getHandler(String methodName, Map<String, Object> handlers) {
Object handler = null;
String handlerName = null;
int dot = methodName.lastIndexOf('.');
if (dot > -1) {
handlerName = methodName.substring(0, dot);
handler = handlers.get(handlerName);
}
if (handler != null) {
return handler;
}
IllegalStateException exception;
if (dot > -1) {
exception = new IllegalStateException("RPC handler object \"" + handlerName + "\" not found");
}
else {
exception = new IllegalStateException("RPC handler object not found for \"" + methodName);
}
LOG.error(exception);
throw exception;
}
private static class XmlRpcHandlerMappingImpl implements XmlRpcHandlerMapping {
protected final THashMap<String, Object> handlers = new THashMap<String, Object>();
public void addHandler(@NotNull String handlerName, @NotNull Object handler) {
if (handler instanceof XmlRpcHandler) {
handlers.put(handlerName, handler);
}
else {
handlers.put(handlerName, new Invoker(handler));
}
}
public void removeHandler(String handlerName) {
handlers.remove(handlerName);
}
@Override
public Object getHandler(String methodName) {
Object handler = null;
String handlerName = null;
int dot = methodName.lastIndexOf('.');
if (dot > -1) {
handlerName = methodName.substring(0, dot);
handler = handlers.get(handlerName);
}
if (handler != null) {
return handler;
}
IllegalStateException exception;
if (dot > -1) {
exception = new IllegalStateException("RPC handler object \"" + handlerName + "\" not found");
}
else {
exception = new IllegalStateException("RPC handler object not found for \"" + methodName);
}
LOG.error(exception);
throw exception;
}
}
private static class LoggingDefaultHandlerMapping extends XmlRpcHandlerMappingImpl {
@Override
public void addHandler(@NotNull String handlerName, @NotNull Object handler) {
LOG.debug(String.format("addHandler: handlerName: %s, handler: %s%s", handlerName, handler, getHandlers()));
super.addHandler(handlerName, handler);
}
@Override
public void removeHandler(String handlerName) {
LOG.debug(String.format("removeHandler: handlerName: %s%s", handlerName, getHandlers()));
super.removeHandler(handlerName);
}
@Override
public Object getHandler(String methodName) {
LOG.debug(String.format("getHandler: methodName: %s%s", methodName, getHandlers()));
return super.getHandler(methodName);
}
private String getHandlers() {
//noinspection SpellCheckingInspection
return String.format("%nhandlers: %s %s", Arrays.toString(handlers.keySet().toArray()), Arrays.toString(handlers.values().toArray()));
}
@Override
public String toString() {
return getHandlers();
}
private static Object invokeHandler(@NotNull Object handler, XmlRpcServerRequest request) throws Exception {
return (handler instanceof XmlRpcHandler ? (XmlRpcHandler)handler : new Invoker(handler)).execute(request.getMethodName(), request.getParameters());
}
}
@@ -37,7 +37,7 @@ public class BuiltInServerManagerImpl extends BuiltInServerManager {
@Nullable
private BuiltInServer server;
private boolean myEnabledInUnitTestMode = true;
private boolean enabledInUnitTestMode = true;
@Override
public int getPort() {
@@ -83,7 +83,7 @@ public class BuiltInServerManagerImpl extends BuiltInServerManager {
private Future<?> startServerInPooledThread() {
Application application = ApplicationManager.getApplication();
if (application.isUnitTestMode() && !myEnabledInUnitTestMode) {
if (application.isUnitTestMode() && !enabledInUnitTestMode) {
return null;
}
@@ -153,7 +153,6 @@ public class BuiltInServerManagerImpl extends BuiltInServerManager {
*/
@TestOnly
public void setEnabledInUnitTestMode(boolean enabled) {
myEnabledInUnitTestMode = enabled;
enabledInUnitTestMode = enabled;
}
}
@@ -15,6 +15,7 @@
*/
package org.jetbrains.io;
import com.intellij.ide.XmlRpcServer;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.util.Disposer;
@@ -23,17 +24,19 @@ import org.jboss.netty.channel.*;
import org.jboss.netty.channel.group.ChannelGroup;
import org.jboss.netty.channel.group.DefaultChannelGroup;
import org.jboss.netty.channel.socket.nio.NioServerSocketChannelFactory;
import org.jboss.netty.handler.codec.http.*;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.ide.CustomPortServerManager;
import org.jetbrains.ide.PooledThreadExecutor;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.util.Map;
import java.util.concurrent.Executor;
import static org.jboss.netty.channel.Channels.pipeline;
public class BuiltInServer implements Disposable {
private final ChannelGroup openChannels = new DefaultChannelGroup();
@@ -63,17 +66,34 @@ public class BuiltInServer implements Disposable {
throw new IllegalStateException("server already started");
}
ServerBootstrap bootstrap = createServerBootstrap(channelFactory, openChannels);
ServerBootstrap bootstrap = createServerBootstrap(channelFactory, openChannels, null);
int port = bind(firstPort, portsCount, tryAnyPort, bootstrap);
bindCustomPorts(firstPort, port);
return port;
}
static ServerBootstrap createServerBootstrap(NioServerSocketChannelFactory channelFactory, ChannelGroup openChannels) {
static ServerBootstrap createServerBootstrap(NioServerSocketChannelFactory channelFactory, ChannelGroup openChannels, @Nullable Map<String, Object> xmlRpcHandlers) {
ServerBootstrap bootstrap = new ServerBootstrap(channelFactory);
bootstrap.setOption("child.tcpNoDelay", true);
bootstrap.setOption("child.keepAlive", true);
bootstrap.setPipelineFactory(new ChannelPipelineFactoryImpl(new PortUnificationServerHandler(openChannels)));
if (xmlRpcHandlers == null) {
final ChannelHandler handler = new PortUnificationServerHandler(openChannels);
bootstrap.setPipelineFactory(new ChannelPipelineFactory() {
@Override
public ChannelPipeline getPipeline() throws Exception {
return Channels.pipeline(handler);
}
});
}
else {
final XmlRpcDelegatingHttpRequestHandler handler = new XmlRpcDelegatingHttpRequestHandler(xmlRpcHandlers);
bootstrap.setPipelineFactory(new ChannelPipelineFactory() {
@Override
public ChannelPipeline getPipeline() throws Exception {
return Channels.pipeline(new HttpRequestDecoder(), new HttpChunkAggregator(1048576), new HttpResponseEncoder(), handler);
}
});
}
return bootstrap;
}
@@ -157,16 +177,17 @@ public class BuiltInServer implements Disposable {
context.getPipeline().replace(DelegatingHttpRequestHandler.class, "replacedDefaultHandler", messageChannelHandler);
}
private static class ChannelPipelineFactoryImpl implements ChannelPipelineFactory {
private final ChannelHandler defaultHandler;
private static final class XmlRpcDelegatingHttpRequestHandler extends DelegatingHttpRequestHandlerBase {
private final Map<String, Object> handlers;
public ChannelPipelineFactoryImpl(ChannelHandler defaultHandler) {
this.defaultHandler = defaultHandler;
public XmlRpcDelegatingHttpRequestHandler(Map<String, Object> handlers) {
this.handlers = handlers;
}
@Override
public ChannelPipeline getPipeline() throws Exception {
return pipeline(defaultHandler);
protected boolean process(ChannelHandlerContext context, HttpRequest request, QueryStringDecoder urlDecoder) throws IOException {
return (request.getMethod() == HttpMethod.POST || request.getMethod() == HttpMethod.OPTIONS) &&
XmlRpcServer.SERVICE.getInstance().process(urlDecoder.getPath(), request, context, handlers);
}
}
}
@@ -19,8 +19,10 @@ import com.intellij.openapi.application.ex.ApplicationInfoEx;
import com.intellij.openapi.util.IconLoader;
import com.intellij.util.ui.UIUtil;
import org.apache.sanselan.ImageFormat;
import org.apache.sanselan.ImageWriteException;
import org.apache.sanselan.Sanselan;
import org.jboss.netty.channel.*;
import org.jboss.netty.channel.ChannelHandler;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.handler.codec.http.HttpRequest;
import org.jboss.netty.handler.codec.http.QueryStringDecoder;
import org.jetbrains.ide.BuiltInServerManager;
@@ -28,29 +30,16 @@ import org.jetbrains.ide.HttpRequestHandler;
import javax.swing.*;
import java.awt.image.BufferedImage;
import static org.jboss.netty.handler.codec.http.HttpResponseStatus.NOT_FOUND;
import java.io.IOException;
@ChannelHandler.Sharable
final class DelegatingHttpRequestHandler extends SimpleChannelUpstreamHandler {
final class DelegatingHttpRequestHandler extends DelegatingHttpRequestHandlerBase {
@Override
public void messageReceived(ChannelHandlerContext context, MessageEvent event) throws Exception {
if (!(event.getMessage() instanceof HttpRequest)) {
context.sendUpstream(event);
return;
}
HttpRequest request = (HttpRequest)event.getMessage();
//if (BuiltInServer.LOG.isDebugEnabled()) {
//BuiltInServer.LOG.debug(request.toString());
//}
QueryStringDecoder urlDecoder = new QueryStringDecoder(request.getUri());
protected boolean process(ChannelHandlerContext context, HttpRequest request, QueryStringDecoder urlDecoder) throws IOException, ImageWriteException {
HttpRequestHandler connectedHandler = (HttpRequestHandler)context.getAttachment();
if (connectedHandler != null) {
if (connectedHandler.isSupported(request) && connectedHandler.process(urlDecoder, request, context)) {
return;
return true;
}
// prev cached connectedHandler is not suitable for this request, so, let's find it again
context.setAttachment(null);
@@ -63,7 +52,7 @@ final class DelegatingHttpRequestHandler extends SimpleChannelUpstreamHandler {
icon.paintIcon(null, image.getGraphics(), 0, 0);
byte[] icoBytes = Sanselan.writeImageToBytes(image, ImageFormat.IMAGE_FORMAT_ICO, null);
Responses.send(icoBytes, FileResponses.createResponse(urlDecoder.getPath()), request, context);
return;
return true;
}
}
@@ -73,24 +62,13 @@ final class DelegatingHttpRequestHandler extends SimpleChannelUpstreamHandler {
if (context.getAttachment() == null) {
context.setAttachment(handler);
}
return;
return true;
}
}
catch (Throwable e) {
BuiltInServer.LOG.error(e);
}
}
Responses.sendStatus(request, context, NOT_FOUND);
}
@Override
public void exceptionCaught(ChannelHandlerContext context, ExceptionEvent event) throws Exception {
try {
BuiltInServer.LOG.error(event.getCause());
}
finally {
context.setAttachment(null);
event.getChannel().close();
}
return false;
}
}
@@ -0,0 +1,42 @@
package org.jetbrains.io;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.channel.ExceptionEvent;
import org.jboss.netty.channel.MessageEvent;
import org.jboss.netty.channel.SimpleChannelUpstreamHandler;
import org.jboss.netty.handler.codec.http.HttpRequest;
import org.jboss.netty.handler.codec.http.QueryStringDecoder;
import static org.jboss.netty.handler.codec.http.HttpResponseStatus.NOT_FOUND;
abstract class DelegatingHttpRequestHandlerBase extends SimpleChannelUpstreamHandler {
@Override
public final void messageReceived(ChannelHandlerContext context, MessageEvent event) throws Exception {
if (!(event.getMessage() instanceof HttpRequest)) {
context.sendUpstream(event);
return;
}
HttpRequest request = (HttpRequest)event.getMessage();
//if (BuiltInServer.LOG.isDebugEnabled()) {
//BuiltInServer.LOG.debug(request.toString());
//}
if (!process(context, request, new QueryStringDecoder(request.getUri()))) {
Responses.sendStatus(request, context, NOT_FOUND);
}
}
protected abstract boolean process(ChannelHandlerContext context, HttpRequest request, QueryStringDecoder urlDecoder) throws Exception;
@Override
public final void exceptionCaught(ChannelHandlerContext context, ExceptionEvent event) throws Exception {
try {
BuiltInServer.LOG.error(event.getCause());
}
finally {
context.setAttachment(null);
event.getChannel().close();
}
}
}
@@ -34,7 +34,7 @@ final class SubServer implements CustomPortServerManager.CustomPortService, Disp
public SubServer(CustomPortServerManager user, NioServerSocketChannelFactory channelFactory) {
this.user = user;
user.setManager(this);
bootstrap = BuiltInServer.createServerBootstrap(channelFactory, openChannels);
bootstrap = BuiltInServer.createServerBootstrap(channelFactory, openChannels, user.createXmlRpcHandlers());
}
public boolean bind(int port) {
@@ -59,7 +59,6 @@ final class SubServer implements CustomPortServerManager.CustomPortService, Disp
}
private void stop() {
// todo should we call releaseExternalResources? We use only 1 boss&worker thread
openChannels.close().awaitUninterruptibly();
openChannels.clear();
}