http request handlers should not be netty (third-party dependency) channel handlers

xml rpc server serve now only "/" and "/RPC2" (case-insensitive) urls
This commit is contained in:
Vladimir Krivosheev
2013-02-11 18:39:38 +04:00
parent 3d631d0241
commit a7c91ae38a
9 changed files with 175 additions and 120 deletions
@@ -1,5 +1,5 @@
/*
* Copyright 2000-2012 JetBrains s.r.o.
* Copyright 2000-2013 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -17,9 +17,6 @@ package com.intellij.ide;
import com.intellij.openapi.components.ServiceManager;
/**
* @author mike
*/
public interface XmlRpcServer {
void addHandler(String name, Object handler);
boolean hasHandler(String name);
@@ -0,0 +1,32 @@
/*
* Copyright 2000-2013 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.ide;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.handler.codec.http.HttpMethod;
import org.jboss.netty.handler.codec.http.HttpRequest;
import org.jboss.netty.handler.codec.http.QueryStringDecoder;
import java.io.IOException;
public abstract class HttpRequestHandler {
public boolean isSupported(HttpMethod method) {
return method == HttpMethod.GET;
}
public abstract boolean process(QueryStringDecoder urlDecoder, HttpRequest request, ChannelHandlerContext context)
throws IOException;
}
@@ -1,15 +1,27 @@
/*
* Copyright 2000-2013 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.ide;
import com.intellij.openapi.components.ServiceManager;
import com.intellij.openapi.extensions.ExtensionPointName;
import com.intellij.util.Consumer;
import org.jboss.netty.channel.ChannelFutureListener;
import org.jboss.netty.channel.ChannelPipeline;
public abstract class WebServerManager {
// Your handler will be instantiated on first user request
public static final ExtensionPointName<Consumer<ChannelPipeline>> EP_NAME =
ExtensionPointName.create("com.intellij.serverPipelineConsumer");
public static final ExtensionPointName<HttpRequestHandler> EP_NAME = ExtensionPointName.create("com.intellij.httpRequestHandler");
public static WebServerManager getInstance() {
return ServiceManager.getService(WebServerManager.class);
@@ -1,5 +1,5 @@
/*
* Copyright 2000-2012 JetBrains s.r.o.
* Copyright 2000-2013 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -17,24 +17,25 @@ package com.intellij.ide;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.extensions.Extensions;
import com.intellij.util.Consumer;
import gnu.trove.THashMap;
import org.apache.xmlrpc.*;
import org.jboss.netty.buffer.ChannelBuffer;
import org.jboss.netty.buffer.ChannelBufferInputStream;
import org.jboss.netty.buffer.ChannelBuffers;
import org.jboss.netty.channel.*;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.handler.codec.http.HttpMethod;
import org.jboss.netty.handler.codec.http.HttpRequest;
import org.jboss.netty.handler.codec.http.HttpResponse;
import org.jboss.netty.handler.codec.http.QueryStringDecoder;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.ide.HttpRequestHandler;
import org.jetbrains.io.Responses;
import java.io.IOException;
import java.util.Arrays;
@ChannelHandler.Sharable
public class XmlRpcServerImpl extends SimpleChannelUpstreamHandler implements XmlRpcServer {
public class XmlRpcServerImpl implements XmlRpcServer {
private static final Logger LOG = Logger.getInstance(XmlRpcServerImpl.class);
private final XmlRpcHandlerMappingImpl handlerMapping;
@@ -76,11 +77,15 @@ public class XmlRpcServerImpl extends SimpleChannelUpstreamHandler implements Xm
LOG.debug("XmlRpcServerImpl instantiated, handlers " + handlerMapping);
}
static final class XmlRpcPipelineConsumer implements Consumer<ChannelPipeline> {
static final class XmlRpcRequestHandler extends HttpRequestHandler {
@Override
public void consume(ChannelPipeline pipeline) {
XmlRpcServer xmlRpcServer = SERVICE.getInstance();
pipeline.addLast("pluggable_xmlRpc", (XmlRpcServerImpl)xmlRpcServer);
public boolean isSupported(HttpMethod method) {
return method == HttpMethod.POST || method == HttpMethod.OPTIONS;
}
@Override
public boolean process(QueryStringDecoder urlDecoder, HttpRequest request, ChannelHandlerContext context) throws IOException {
return ((XmlRpcServerImpl)SERVICE.getInstance()).process(urlDecoder, request, context);
}
}
@@ -97,52 +102,40 @@ public class XmlRpcServerImpl extends SimpleChannelUpstreamHandler implements Xm
handlerMapping.removeHandler(name);
}
public void messageReceived(ChannelHandlerContext context, MessageEvent e) throws Exception {
if (e.getMessage() instanceof HttpRequest) {
HttpRequest request = (HttpRequest)e.getMessage();
private boolean process(QueryStringDecoder urlDecoder, HttpRequest request, ChannelHandlerContext context) throws IOException {
if (!(urlDecoder.getPath().isEmpty() || urlDecoder.getPath().equalsIgnoreCase("/RPC2"))) {
return false;
}
if (request.getMethod() == HttpMethod.POST) {
ChannelBuffer result;
ChannelBufferInputStream in = new ChannelBufferInputStream(request.getContent());
try {
result = ChannelBuffers.copiedBuffer(new XmlRpcWorker(handlerMapping).execute(in, xmlRpcContext));
}
catch (Throwable ex) {
context.getChannel().close();
LOG.error(ex);
return;
}
finally {
in.close();
}
HttpResponse response = Responses.create("text/xml");
response.setContent(result);
Responses.send(response, request, context);
return;
if (request.getMethod() == HttpMethod.POST) {
ChannelBuffer result;
ChannelBufferInputStream in = new ChannelBufferInputStream(request.getContent());
try {
result = ChannelBuffers.copiedBuffer(new XmlRpcWorker(handlerMapping).execute(in, xmlRpcContext));
}
catch (Throwable ex) {
context.getChannel().close();
LOG.error(ex);
return true;
}
finally {
in.close();
}
if (request.getMethod() == HttpMethod.OPTIONS &&
HttpMethod.POST.getName().equals(request.getHeader("Access-Control-Request-Method"))) {
HttpResponse response = Responses.create("text/plain");
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
Responses.send(response, request, context);
return;
}
HttpResponse response = Responses.create("text/xml");
response.setContent(result);
Responses.send(response, request, context);
return true;
}
context.sendUpstream(e);
}
@Override
public void exceptionCaught(ChannelHandlerContext context, ExceptionEvent e) throws Exception {
try {
LOG.error(e.getCause());
}
finally {
e.getChannel().close();
else if (HttpMethod.POST.getName().equals(request.getHeader("Access-Control-Request-Method"))) {
assert request.getMethod() == HttpMethod.OPTIONS;
HttpResponse response = Responses.create("text/plain");
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
Responses.send(response, request, context);
return true;
}
return false;
}
private static class XmlRpcHandlerMappingImpl implements XmlRpcHandlerMapping {
@@ -9,16 +9,11 @@ import com.intellij.openapi.application.Application;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ApplicationNamesInfo;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.extensions.Extensions;
import com.intellij.openapi.project.DumbAware;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.startup.StartupActivity;
import com.intellij.openapi.util.Computable;
import com.intellij.openapi.util.SimpleTimer;
import com.intellij.util.Consumer;
import org.jboss.netty.channel.ChannelException;
import org.jboss.netty.channel.ChannelFutureListener;
import org.jboss.netty.channel.ChannelPipeline;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.io.WebServer;
@@ -114,18 +109,7 @@ class WebServerManagerImpl extends WebServerManager implements Disposable {
return;
}
detectedPortNumber = server.start(getDefaultPort(), PORTS_COUNT, true, new Computable<Consumer<ChannelPipeline>[]>() {
@Override
public Consumer<ChannelPipeline>[] compute() {
Consumer<ChannelPipeline>[] consumers = Extensions.getExtensions(EP_NAME);
if (consumers.length == 0) {
LOG.warn("web server will be stopped, there are no pipeline consumers");
SimpleTimer.getInstance().setUp(server.createShutdownTask(), 3000);
}
return consumers;
}
});
detectedPortNumber = server.start(getDefaultPort(), PORTS_COUNT, true);
if (detectedPortNumber == -1) {
LOG.info("web server cannot be started, cannot bind to port");
}
@@ -1,9 +1,25 @@
/*
* Copyright 2000-2013 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.io;
import com.intellij.openapi.application.Application;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ex.ApplicationInfoEx;
import org.jboss.netty.buffer.BigEndianHeapChannelBuffer;
import org.jboss.netty.buffer.ChannelBuffer;
import org.jboss.netty.buffer.ChannelBuffers;
import org.jboss.netty.channel.ChannelFuture;
import org.jboss.netty.channel.ChannelFutureListener;
@@ -74,7 +90,10 @@ public final class Responses {
}
public static void send(HttpResponse response, HttpRequest request, ChannelHandlerContext context) {
setContentLength(response, response.getContent().readableBytes());
ChannelBuffer content = response.getContent();
if (content != ChannelBuffers.EMPTY_BUFFER) {
setContentLength(response, content.readableBytes());
}
send(response, context, !isKeepAlive(request));
}
@@ -17,10 +17,8 @@ package org.jetbrains.io;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.util.Computable;
import com.intellij.openapi.util.ShutDownTracker;
import com.intellij.openapi.util.SystemInfo;
import com.intellij.util.Consumer;
import com.intellij.util.concurrency.Semaphore;
import com.intellij.util.containers.ContainerUtil;
import org.jboss.netty.bootstrap.ClientBootstrap;
@@ -35,7 +33,9 @@ import org.jboss.netty.handler.codec.http.*;
import org.jboss.netty.util.CharsetUtil;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.ide.HttpRequestHandler;
import org.jetbrains.ide.PooledThreadExecutor;
import org.jetbrains.ide.WebServerManager;
import java.net.InetAddress;
import java.net.InetSocketAddress;
@@ -71,26 +71,18 @@ public class WebServer {
return !openChannels.isEmpty();
}
public void start(int port, Consumer<ChannelPipeline>... pipelineConsumers) {
start(port, new Computable.PredefinedValueComputable<Consumer<ChannelPipeline>[]>(pipelineConsumers));
public void start(int port) {
start(port, 1, false);
}
public void start(int port, int portsCount, Consumer<ChannelPipeline>... pipelineConsumers) {
start(port, portsCount, false, new Computable.PredefinedValueComputable<Consumer<ChannelPipeline>[]>(pipelineConsumers));
}
public void start(int port, Computable<Consumer<ChannelPipeline>[]> pipelineConsumers) {
start(port, 1, false, pipelineConsumers);
}
public int start(int firstPort, int portsCount, boolean tryAnyPort, Computable<Consumer<ChannelPipeline>[]> pipelineConsumers) {
public int start(int firstPort, int portsCount, boolean tryAnyPort) {
if (isRunning()) {
throw new IllegalStateException("server already started");
}
ServerBootstrap bootstrap = new ServerBootstrap(channelFactory);
bootstrap.setOption("child.tcpNoDelay", true);
bootstrap.setPipelineFactory(new ChannelPipelineFactoryImpl(pipelineConsumers, new DefaultHandler(openChannels)));
bootstrap.setPipelineFactory(new ChannelPipelineFactoryImpl(new DefaultHandler(openChannels)));
return bind(firstPort, portsCount, tryAnyPort, bootstrap);
}
@@ -307,28 +299,20 @@ public class WebServer {
}
}
public static void replaceDefaultHandler(@NotNull ChannelHandlerContext context, @NotNull SimpleChannelUpstreamHandler messageChannelHandler) {
context.getPipeline().replace(DefaultHandler.class, "replacedDefaultHandler", messageChannelHandler);
}
private static class ChannelPipelineFactoryImpl implements ChannelPipelineFactory {
private final Computable<Consumer<ChannelPipeline>[]> pipelineConsumers;
private final DefaultHandler defaultHandler;
public ChannelPipelineFactoryImpl(Computable<Consumer<ChannelPipeline>[]> pipelineConsumers, DefaultHandler defaultHandler) {
this.pipelineConsumers = pipelineConsumers;
public ChannelPipelineFactoryImpl(DefaultHandler defaultHandler) {
this.defaultHandler = defaultHandler;
}
@Override
public ChannelPipeline getPipeline() throws Exception {
ChannelPipeline pipeline = pipeline(new HttpRequestDecoder(), new HttpChunkAggregator(1048576), new HttpResponseEncoder());
for (Consumer<ChannelPipeline> consumer : pipelineConsumers.compute()) {
try {
consumer.consume(pipeline);
}
catch (Throwable e) {
LOG.error(e);
}
}
pipeline.addLast("defaultHandler", defaultHandler);
return pipeline;
return pipeline(new HttpRequestDecoder(), new HttpChunkAggregator(1048576), new HttpResponseEncoder(), defaultHandler);
}
}
@@ -346,21 +330,55 @@ public class WebServer {
}
@Override
public void messageReceived(ChannelHandlerContext context, MessageEvent e) throws Exception {
if (e.getMessage() instanceof HttpRequest) {
HttpRequest message = (HttpRequest)e.getMessage();
if (new QueryStringDecoder(message.getUri()).getPath().equals(START_TIME_PATH)) {
HttpResponse response = new DefaultHttpResponse(HTTP_1_1, OK);
response.setHeader("Access-Control-Allow-Origin", "*");
response.setContent(ChannelBuffers.copiedBuffer(getApplicationStartTime(), CharsetUtil.US_ASCII));
Responses.addServer(response);
Responses.addDate(response);
context.getChannel().write(response).addListener(ChannelFutureListener.CLOSE);
}
else {
Responses.sendError(message, context, NOT_FOUND);
public void messageReceived(ChannelHandlerContext context, MessageEvent event) throws Exception {
if (!(event.getMessage() instanceof HttpRequest)) {
context.sendUpstream(event);
}
HttpRequest request = (HttpRequest)event.getMessage();
QueryStringDecoder urlDecoder = new QueryStringDecoder(request.getUri());
if (urlDecoder.getPath().equals(START_TIME_PATH)) {
HttpResponse response = new DefaultHttpResponse(HTTP_1_1, OK);
response.setHeader("Access-Control-Allow-Origin", "*");
response.setContent(ChannelBuffers.copiedBuffer(getApplicationStartTime(), CharsetUtil.US_ASCII));
Responses.addServer(response);
Responses.addDate(response);
Responses.send(response, context);
return;
}
HttpRequestHandler connectedHandler = (HttpRequestHandler)context.getAttachment();
if (connectedHandler == null) {
for (HttpRequestHandler handler : WebServerManager.EP_NAME.getExtensions()) {
try {
if (handler.isSupported(request.getMethod()) && handler.process(urlDecoder, request, context)) {
if (context.getAttachment() == null) {
context.setAttachment(handler);
}
return;
}
}
catch (Throwable e) {
LOG.error(e);
}
}
}
else if (connectedHandler.isSupported(request.getMethod())) {
connectedHandler.process(urlDecoder, request, context);
return;
}
Responses.sendError(request, context, NOT_FOUND);
}
@Override
public void exceptionCaught(ChannelHandlerContext context, ExceptionEvent event) throws Exception {
try {
LOG.error(event.getCause());
}
finally {
context.setAttachment(null);
event.getChannel().close();
}
}
}
}
@@ -149,7 +149,7 @@
<extensionPoint name="pathMacroFilter" interface="com.intellij.openapi.application.PathMacroFilter"/>
<extensionPoint name="pathMacroExpendableProtocol" beanClass="com.intellij.application.options.PathMacroExpendableProtocolBean"/>
<extensionPoint name="serverPipelineConsumer" interface="com.intellij.util.Consumer"/>
<extensionPoint name="httpRequestHandler" interface="org.jetbrains.ide.HttpRequestHandler"/>
<extensionPoint name="colorPickerListenerFactory" interface="com.intellij.ui.ColorPickerListenerFactory"/>
</extensionPoints>
@@ -289,7 +289,7 @@
<applicationService serviceInterface="org.jetbrains.ide.WebServerManager" serviceImplementation="org.jetbrains.ide.WebServerManagerImpl"/>
<applicationService serviceInterface="com.intellij.ide.XmlRpcServer" serviceImplementation="com.intellij.ide.XmlRpcServerImpl"/>
<serverPipelineConsumer implementation="com.intellij.ide.XmlRpcServerImpl$XmlRpcPipelineConsumer"/>
<httpRequestHandler implementation="com.intellij.ide.XmlRpcServerImpl$XmlRpcRequestHandler"/>
<postStartupActivity implementation="org.jetbrains.ide.WebServerManagerImpl$MyPostStartupActivity"/>
<editorNotificationProvider implementation="com.intellij.ide.FileChangedNotificationProvider"/>