001package org.xbib.elasticsearch.http;
002
003import org.elasticsearch.ElasticsearchException;
004import org.elasticsearch.ElasticsearchIllegalArgumentException;
005import org.elasticsearch.common.collect.ImmutableMap;
006import org.elasticsearch.common.component.AbstractLifecycleComponent;
007import org.elasticsearch.common.inject.Inject;
008import org.elasticsearch.common.io.Streams;
009import org.elasticsearch.common.settings.Settings;
010import org.elasticsearch.common.transport.TransportAddress;
011import org.elasticsearch.env.Environment;
012import org.elasticsearch.node.service.NodeService;
013import org.elasticsearch.rest.BytesRestResponse;
014import org.elasticsearch.rest.RestChannel;
015import org.elasticsearch.rest.RestController;
016import org.elasticsearch.rest.RestFilter;
017import org.elasticsearch.rest.RestFilterChain;
018import org.elasticsearch.rest.RestRequest;
019import org.elasticsearch.rest.RestStatus;
020import org.jboss.netty.channel.Channel;
021import org.jboss.netty.channel.ChannelHandlerContext;
022import org.jboss.netty.handler.codec.http.websocketx.WebSocketFrame;
023import org.jboss.netty.handler.codec.http.websocketx.WebSocketServerHandshaker;
024import org.xbib.elasticsearch.rest.HttpPatchRestController;
025import org.xbib.elasticsearch.websocket.InteractiveController;
026import org.xbib.elasticsearch.websocket.Presence;
027
028import java.io.File;
029import java.io.IOException;
030import java.util.HashMap;
031import java.util.Locale;
032import java.util.Map;
033
034import static org.elasticsearch.rest.RestStatus.FORBIDDEN;
035import static org.elasticsearch.rest.RestStatus.INTERNAL_SERVER_ERROR;
036import static org.elasticsearch.rest.RestStatus.NOT_FOUND;
037import static org.elasticsearch.rest.RestStatus.OK;
038
039public class HttpServer extends AbstractLifecycleComponent<HttpServer> {
040
041    private final Environment environment;
042
043    private final HttpServerTransport transport;
044
045    private final RestController restController;
046
047    private final HttpPatchRestController httpPatchRestController;
048
049    private final InteractiveController interActiveController;
050
051    private final NodeService nodeService;
052
053    private final boolean disableSites;
054
055    private final PluginSiteFilter pluginSiteFilter = new PluginSiteFilter();
056
057    @Inject
058    public HttpServer(Settings settings, Environment environment,
059                      HttpServerTransport transport,
060                      RestController restController,
061                      HttpPatchRestController httpPatchRestController,
062                      InteractiveController interActiveController,
063                      NodeService nodeService) {
064        super(settings);
065        this.environment = environment;
066        this.transport = transport;
067        this.restController = restController;
068        this.httpPatchRestController = httpPatchRestController;
069        this.interActiveController = interActiveController;
070        this.nodeService = nodeService;
071
072        this.disableSites = componentSettings.getAsBoolean("disable_sites", false);
073
074        transport.httpServerAdapter(new Dispatcher(this));
075
076        transport.webSocketServerAdapter(new Interactor(this));
077
078    }
079
080    static class Dispatcher implements HttpServerAdapter {
081
082        private final HttpServer server;
083
084        Dispatcher(HttpServer server) {
085            this.server = server;
086        }
087
088        @Override
089        public void dispatchRequest(HttpRequest request, HttpChannel channel) {
090            server.internalDispatchRequest(request, channel);
091        }
092    }
093
094    static class Interactor implements WebSocketServerAdapter {
095
096        private final HttpServer server;
097
098        Interactor(HttpServer server) {
099            this.server = server;
100        }
101
102        @Override
103        public void presence(Presence presence, String topic, Channel channel) {
104            server.internalPresence(presence, topic, channel);
105        }
106
107        @Override
108        public void frame(WebSocketServerHandshaker handshaker, WebSocketFrame frame, ChannelHandlerContext context) {
109            server.internalFrame(handshaker, frame, context);
110        }
111    }
112
113    @Override
114    protected void doStart() throws ElasticsearchException {
115        transport.start();
116        logger.info("{}", transport.boundAddress());
117        nodeService.putAttribute("websocket_address", transport.boundAddress().publishAddress().toString());
118    }
119
120    @Override
121    protected void doStop() throws ElasticsearchException {
122        nodeService.removeAttribute("websocket_address");
123        transport.stop();
124    }
125
126    @Override
127    protected void doClose() throws ElasticsearchException {
128        transport.close();
129    }
130
131    public TransportAddress address() {
132        return transport.boundAddress().publishAddress();
133    }
134
135    public HttpInfo info() {
136        return transport.info();
137    }
138
139    public HttpStats stats() {
140        return transport.stats();
141    }
142
143    public Channel channel(Integer id) {
144        return transport.channel(id);
145    }
146
147    public void internalDispatchRequest(final HttpRequest request, final HttpChannel channel) {
148        if (request.rawPath().startsWith("/_plugin/")) {
149            RestFilterChain filterChain = restController.filterChain(pluginSiteFilter);
150            filterChain.continueProcessing(request, channel);
151            return;
152        }
153        try {
154            restController.dispatchRequest(request, channel);
155        } catch (ElasticsearchIllegalArgumentException e) {
156            // unsupported HTTP method, try HTTP PATCH
157            httpPatchRestController.dispatchRequest(request, channel);
158        }
159    }
160
161    public void internalPresence(Presence presence, String topic, Channel channel) {
162        interActiveController.presence(presence, topic, channel);
163    }
164
165    public void internalFrame(WebSocketServerHandshaker handshaker, WebSocketFrame frame, ChannelHandlerContext context) {
166        interActiveController.frame(handshaker, frame, context);
167    }
168
169
170    class PluginSiteFilter extends RestFilter {
171
172        @Override
173        public void process(RestRequest request, RestChannel channel, RestFilterChain filterChain) {
174            handlePluginSite((HttpRequest) request, (HttpChannel) channel);
175        }
176    }
177
178    void handlePluginSite(HttpRequest request, HttpChannel channel) {
179        if (disableSites) {
180            channel.sendResponse(new BytesRestResponse(FORBIDDEN));
181            return;
182        }
183        if (request.method().name().equals("OPTIONS")) {
184            // when we have OPTIONS request, simply send OK by default (with the Access Control Origin header which gets automatically added)
185            channel.sendResponse(new BytesRestResponse(OK));
186            return;
187        }
188        if (request.method().name().equals("GET")) {
189            channel.sendResponse(new BytesRestResponse(FORBIDDEN));
190            return;
191        }
192        // TODO for a "/_plugin" endpoint, we should have a page that lists all the plugins?
193
194        String path = request.rawPath().substring("/_plugin/".length());
195        int i1 = path.indexOf('/');
196        String pluginName;
197        String sitePath;
198        if (i1 == -1) {
199            pluginName = path;
200            sitePath = null;
201            // If a trailing / is missing, we redirect to the right page #2654
202            channel.sendResponse(new BytesRestResponse(RestStatus.MOVED_PERMANENTLY, "text/html", "<head><meta http-equiv=\"refresh\" content=\"0; URL=" + request.rawPath() + "/\"></head>"));
203            return;
204        } else {
205            pluginName = path.substring(0, i1);
206            sitePath = path.substring(i1 + 1);
207        }
208
209        if (sitePath.length() == 0) {
210            sitePath = "/index.html";
211        }
212
213        // Convert file separators.
214        sitePath = sitePath.replace('/', File.separatorChar);
215
216        // this is a plugin provided site, serve it as static files from the plugin location
217        File siteFile = new File(new File(environment.pluginsFile(), pluginName), "_site");
218        File file = new File(siteFile, sitePath);
219        if (!file.exists() || file.isHidden()) {
220            channel.sendResponse(new BytesRestResponse(NOT_FOUND));
221            return;
222        }
223        if (!file.isFile()) {
224            // If it's not a dir, we send a 403
225            if (!file.isDirectory()) {
226                channel.sendResponse(new BytesRestResponse(FORBIDDEN));
227                return;
228            }
229            // We don't serve dir but if index.html exists in dir we should serve it
230            file = new File(file, "index.html");
231            if (!file.exists() || file.isHidden() || !file.isFile()) {
232                channel.sendResponse(new BytesRestResponse(FORBIDDEN));
233                return;
234            }
235        }
236        if (!file.getAbsolutePath().startsWith(siteFile.getAbsolutePath())) {
237            channel.sendResponse(new BytesRestResponse(FORBIDDEN));
238            return;
239        }
240        try {
241            byte[] data = Streams.copyToByteArray(file);
242            channel.sendResponse(new BytesRestResponse(OK, guessMimeType(sitePath), data));
243        } catch (IOException e) {
244            channel.sendResponse(new BytesRestResponse(INTERNAL_SERVER_ERROR));
245        }
246    }
247
248
249    // TODO: Don't respond with a mime type that violates the request's Accept header
250    private String guessMimeType(String path) {
251        int lastDot = path.lastIndexOf('.');
252        if (lastDot == -1) {
253            return "";
254        }
255        String extension = path.substring(lastDot + 1).toLowerCase(Locale.ROOT);
256        String mimeType = DEFAULT_MIME_TYPES.get(extension);
257        if (mimeType == null) {
258            return "";
259        }
260        return mimeType;
261    }
262
263    static {
264        // This is not an exhaustive list, just the most common types. Call registerMimeType() to add more.
265        Map<String, String> mimeTypes = new HashMap<>();
266        mimeTypes.put("txt", "text/plain");
267        mimeTypes.put("css", "text/css");
268        mimeTypes.put("csv", "text/csv");
269        mimeTypes.put("htm", "text/html");
270        mimeTypes.put("html", "text/html");
271        mimeTypes.put("xml", "text/xml");
272        mimeTypes.put("js", "text/javascript"); // Technically it should be application/javascript (RFC 4329), but IE8 struggles with that
273        mimeTypes.put("xhtml", "application/xhtml+xml");
274        mimeTypes.put("json", "application/json");
275        mimeTypes.put("pdf", "application/pdf");
276        mimeTypes.put("zip", "application/zip");
277        mimeTypes.put("tar", "application/x-tar");
278        mimeTypes.put("gif", "image/gif");
279        mimeTypes.put("jpeg", "image/jpeg");
280        mimeTypes.put("jpg", "image/jpeg");
281        mimeTypes.put("tiff", "image/tiff");
282        mimeTypes.put("tif", "image/tiff");
283        mimeTypes.put("png", "image/png");
284        mimeTypes.put("svg", "image/svg+xml");
285        mimeTypes.put("ico", "image/vnd.microsoft.icon");
286        mimeTypes.put("mp3", "audio/mpeg");
287        DEFAULT_MIME_TYPES = ImmutableMap.copyOf(mimeTypes);
288    }
289
290    public static final Map<String, String> DEFAULT_MIME_TYPES;
291
292}