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}