Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ISPN-10589 REST Resources allow for ambiguous paths #7305

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
10 changes: 10 additions & 0 deletions documentation/src/main/asciidoc/topics/rest_api_v2.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,16 @@ request with the `?action=size` parameter:
include::rest_examples/get_v2_size.adoc[]
----

[[rest_v2_cache_stats]]
=== Getting Cache Statistics

To obtain runtime statistics of a cache invoke a `GET` request:

[source,options="nowrap",subs=attributes+]
----
include::rest_examples/get_v2_stats.adoc[]
----

[[rest_v2_query_cache]]
=== Querying Caches

Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1 @@
GET /rest/v2/configurations/{name}
GET /rest/v2/caches/{name}?action=config
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
GET /rest/v2/caches/{cacheName}?action=stats
Original file line number Diff line number Diff line change
Expand Up @@ -81,4 +81,8 @@ public ScheduledExecutorService getScheduledExecutor() {
public void stop() {
scheduledExecutor.shutdown();
}

public String getContext() {
return configuration.contextPath();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST;
import static io.netty.handler.codec.http.HttpResponseStatus.FORBIDDEN;
import static io.netty.handler.codec.http.HttpResponseStatus.NOT_FOUND;
import static io.netty.handler.codec.http.HttpResponseStatus.REQUEST_ENTITY_TOO_LARGE;
import static io.netty.handler.codec.http.HttpResponseStatus.UNAUTHORIZED;
import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1;
Expand Down Expand Up @@ -77,9 +76,6 @@ public void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) thr
return;
}

if (!restRequest.getContext().equals(this.context)) {
sendResponse(ctx, request, new NettyRestResponse.Builder().status(NOT_FOUND).build());
}

LookupResult invocationLookup = restServer.getRestDispatcher().lookupInvocation(restRequest);

Expand Down
19 changes: 10 additions & 9 deletions server/rest/src/main/java/org/infinispan/rest/RestServer.java
Original file line number Diff line number Diff line change
Expand Up @@ -106,19 +106,20 @@ protected void startInternal(RestServerConfiguration configuration, EmbeddedCach
(EmbeddedCounterManager) EmbeddedCounterManagerFactory.asCounterManager(cacheManager),
configuration, server, getExecutor());

ResourceManager resourceManager = new ResourceManagerImpl(configuration.contextPath());

resourceManager.registerResource(new CacheResource(invocationHelper));
resourceManager.registerResource(new CacheResourceV2(invocationHelper));
resourceManager.registerResource(new SplashResource());
resourceManager.registerResource(new CounterResource(invocationHelper));
resourceManager.registerResource(new CacheManagerResource(invocationHelper));
String restContext = configuration.contextPath();
String staticContext = "/";
ResourceManager resourceManager = new ResourceManagerImpl();
resourceManager.registerResource(staticContext, new SplashResource());
resourceManager.registerResource(restContext, new CacheResource(invocationHelper));
resourceManager.registerResource(restContext, new CacheResourceV2(invocationHelper));
resourceManager.registerResource(restContext, new CounterResource(invocationHelper));
resourceManager.registerResource(restContext, new CacheManagerResource(invocationHelper));
Path staticResources = configuration.staticResources();
if (staticResources != null) {
resourceManager.registerResource(new StaticFileResource(staticResources, "static"));
resourceManager.registerResource(staticContext, new StaticFileResource(staticResources, "static"));
}
if (server != null) {
resourceManager.registerResource(new ServerResource(invocationHelper));
resourceManager.registerResource(restContext, new ServerResource(invocationHelper));
}
this.restDispatcher = new RestDispatcherImpl(resourceManager);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
*/
public interface ResourceManager {

void registerResource(ResourceHandler handler) throws RegistrationException;
void registerResource(String context, ResourceHandler handler) throws RegistrationException;

LookupResult lookupResource(Method method, String path, String action);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,4 +123,15 @@ InvocationImpl build() {
}
}

@Override
public String toString() {
return "InvocationImpl{" +
"methods=" + methods +
", paths=" + paths +
", handler=" + handler +
", action='" + action + '\'' +
", name='" + name + '\'' +
", anonymous=" + anonymous +
'}';
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package org.infinispan.rest.framework.impl;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

import org.infinispan.rest.framework.LookupResult;
import org.infinispan.rest.framework.Method;
import org.infinispan.rest.framework.RegistrationException;
import org.infinispan.rest.framework.ResourceHandler;
import org.infinispan.rest.framework.ResourceManager;
import org.infinispan.rest.logging.Log;
Expand All @@ -19,24 +21,23 @@ public class ResourceManagerImpl implements ResourceManager {

private final static Log logger = LogFactory.getLog(ResourceManagerImpl.class, Log.class);


private final ResourceNode resourceTree;
private final String rootPath;

public ResourceManagerImpl(String rootPath) {
this.rootPath = rootPath;
this.resourceTree = new ResourceNode(new StringPathItem(rootPath), null);
public ResourceManagerImpl() {
this.resourceTree = new ResourceNode(new StringPathItem("/"), null);
}


@Override
public void registerResource(ResourceHandler handler) {
public void registerResource(String context, ResourceHandler handler) throws RegistrationException {
handler.getInvocations().forEach(invocation -> {
Set<String> paths = invocation.paths();
paths.stream().map(this::removeLeadSlash).forEach(path -> {
paths.forEach(path -> {
validate(path);
List<PathItem> p = Arrays.stream(path.split("/")).map(PathItem::fromString).collect(Collectors.toList());
resourceTree.insertPath(invocation, p);
List<PathItem> p = Arrays.stream(path.split("/")).filter(s -> !s.isEmpty()).map(PathItem::fromString).collect(Collectors.toList());
List<PathItem> pathWithCtx = new ArrayList<>();
pathWithCtx.add(new StringPathItem(context));
pathWithCtx.addAll(p);
resourceTree.insertPath(invocation, pathWithCtx);
});
});
}
Expand All @@ -47,19 +48,11 @@ private void validate(String path) {
}
}

private String removeLeadSlash(String path) {
if (path.startsWith("/")) return path.substring(1);
return path;
}

@Override
public LookupResult lookupResource(Method method, String path, String action) {
List<PathItem> pathItems = Arrays.stream(removeLeadSlash(path).split("/"))
.map(PathItem::fromString).collect(Collectors.toList());
PathItem startPath = pathItems.iterator().next();
if (!"*".equals(rootPath) && !rootPath.equals(startPath.getPath())) return null;

return resourceTree.find(method, pathItems.subList(1, pathItems.size()), action);
List<PathItem> pathItems = Arrays.stream(path.replaceAll("//+", "/").split("/"))
.map(s -> s.isEmpty() ? "/" : s).map(PathItem::fromString).collect(Collectors.toList());
return resourceTree.find(method, pathItems, action);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ private void updateTable(Invocation invocation) {
String method = m.toString();
Invocation previous = invocationTable.put(method, invocation);
if (previous != null) {
throw logger.duplicateResource(invocation.getName(), m, pathItem.toString());
throw logger.duplicateResourceMethod(invocation.getName(), m, pathItem.toString());
}
});
} else {
Expand Down Expand Up @@ -68,7 +68,8 @@ public String toString() {

private void dumpTree(StringBuilder builder, ResourceNode node, int ident) {
for (int i = 0; i < ident; i++) builder.append(" ");
builder.append("/").append(node.pathItem);
if (!node.pathItem.getPath().equals("/")) builder.append("/").append(node.pathItem);
else builder.append(node.pathItem);
node.invocationTable.forEach((k, v) -> builder.append(" ").append(k).append(":").append(v));
builder.append("\n");
node.children.forEach((key, value) -> dumpTree(builder, value, ident + 1));
Expand All @@ -77,24 +78,33 @@ private void dumpTree(StringBuilder builder, ResourceNode node, int ident) {
private void insertPathInternal(ResourceNode node, Invocation invocation, List<PathItem> path) {
if (path.size() == 1) {
PathItem next = path.iterator().next();
if (next.getPath().isEmpty()) {
updateTable(invocation);
if (next.getPath().equals("/")) {
node.updateTable(invocation);
return;
}
ResourceNode child = node.children.get(next);

ResourceNode conflict = getConflicts(node, next);
if (conflict != null) {
throw logger.duplicateResource(next.toString(), invocation, conflict.pathItem.toString());
}
if (child == null) {
node.insert(next, invocation);
} else {
child.updateTable(invocation);
}
} else {
PathItem pathItem = path.iterator().next();
ResourceNode child = node.children.get(pathItem);
if (child == null) {
ResourceNode inserted = node.insert(pathItem, null);
insertPathInternal(inserted, invocation, path.subList(1, path.size()));
if (pathItem.getPath().equals("/")) {
insertPathInternal(node, invocation, path.subList(1, path.size()));
} else {
insertPathInternal(child, invocation, path.subList(1, path.size()));
ResourceNode child = node.children.get(pathItem);
if (child == null) {
ResourceNode inserted = node.insert(pathItem, null);
insertPathInternal(inserted, invocation, path.subList(1, path.size()));
} else {
insertPathInternal(child, invocation, path.subList(1, path.size()));
}
}
}
}
Expand All @@ -113,10 +123,21 @@ private ResourceNode findMatch(String path, Map<String, String> variables) {
return null;
}

private ResourceNode getConflicts(ResourceNode node, PathItem candidate) {
Map<PathItem, ResourceNode> children = node.children;
for (Map.Entry<PathItem, ResourceNode> entry : children.entrySet()) {
PathItem pathItem = entry.getKey();
ResourceNode resourceNode = entry.getValue();
if (!pathItem.getClass().equals(candidate.getClass())) return resourceNode;
}
return null;
}

public LookupResult find(Method method, List<PathItem> path, String action) {
ResourceNode current = this;
Map<String, String> variables = new HashMap<>();
for (PathItem pathItem : path) {
if (pathItem.equals(current.pathItem)) continue;
ResourceNode resourceNode = current.children.get(pathItem);
ResourceNode matchAll = current.children.get(new StringPathItem("*"));
if (resourceNode != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import org.infinispan.commons.CacheConfigurationException;
import org.infinispan.commons.dataconversion.EncodingException;
import org.infinispan.rest.cachemanager.exceptions.CacheUnavailableException;
import org.infinispan.rest.framework.Invocation;
import org.infinispan.rest.framework.Method;
import org.infinispan.rest.framework.RegistrationException;
import org.infinispan.rest.operations.exceptions.NoCacheFoundException;
Expand Down Expand Up @@ -67,7 +68,7 @@ public interface Log extends BasicLogger {
CacheConfigurationException illegalCompressionLevel(int compressionLevel);

@Message(value = "Cannot register invocation '%s': resource already registered for method '%s' at the destination path '/%s'", id = 12015)
RegistrationException duplicateResource(String invocationName, Method method, String existingPath);
RegistrationException duplicateResourceMethod(String invocationName, Method method, String existingPath);

@LogMessage(level = WARN)
@Message(value = "Header '%s' will be ignored, expecting a number but got '%s'", id = 12016)
Expand All @@ -78,4 +79,7 @@ public interface Log extends BasicLogger {

@Message(value = "Cannot register invocation with path '%s': '*' is only allowed at the end", id = 12018)
RegistrationException invalidPath(String path);

@Message(value = "Cannot register path '%s' for invocation '%s', since it conflicts with already registered path '%s'", id = 12019)
RegistrationException duplicateResource(String candidate, Invocation invocation, String existingPath);
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@ public Invocations getInvocations() {
.invocation().method(DELETE).path("/v2/caches/{cacheName}/{cacheKey}").handleWith(this::deleteCacheValue)

// Info and statistics
.invocation().methods(GET, HEAD).path("/v2/caches/{cacheName}/config").handleWith(this::getCacheConfig)
.invocation().methods(GET).path("/v2/caches/{cacheName}/stats").handleWith(this::getCacheStats)
.invocation().methods(GET, HEAD).path("/v2/caches/{cacheName}").withAction("config").handleWith(this::getCacheConfig)
.invocation().methods(GET).path("/v2/caches/{cacheName}").withAction("stats").handleWith(this::getCacheStats)

// Cache lifecycle
.invocation().methods(POST).path("/v2/caches/{cacheName}").handleWith(this::createCache)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,18 +51,31 @@ public Invocations getInvocations() {
.create();
}

private File resolve(String resource) {
Path resolved = dir.resolve(resource);
try {
if (!resolved.toFile().getCanonicalPath().startsWith(dir.toAbsolutePath().toString())) {
return null;
}
} catch (IOException e) {
return null;
}
File file = resolved.toFile();
if (!file.isFile() || !file.exists()) {
return null;
}
return file;
}

private CompletionStage<RestResponse> serveFile(RestRequest restRequest) {
NettyRestResponse.Builder responseBuilder = new NettyRestResponse.Builder();

String uri = restRequest.uri();
String resource = uri.substring(uri.indexOf(urlPath) + urlPath.length() + 1);
String resource = uri.equals("/" + urlPath) ? "" : uri.substring(uri.indexOf(urlPath) + urlPath.length() + 1);
if (resource.isEmpty()) resource = DEFAULT_RESOURCE;
Path resolved = dir.resolve(resource);
if (!resolved.toAbsolutePath().startsWith(dir)) {
return CompletableFuture.completedFuture(responseBuilder.status(HttpResponseStatus.NOT_FOUND).build());
}
File file = resolved.toFile();
if (!file.isFile() || !file.exists()) {

File file = resolve(resource);
if (file == null) {
return CompletableFuture.completedFuture(responseBuilder.status(HttpResponseStatus.NOT_FOUND).build());
}

Expand All @@ -87,7 +100,7 @@ private CompletionStage<RestResponse> serveFile(RestRequest restRequest) {

String mediaType = APPLICATION_OCTET_STREAM_TYPE;
try {
String probed = Files.probeContentType(resolved);
String probed = Files.probeContentType(file.toPath());
if (probed != null) mediaType = probed;
} catch (IOException ignored) {
}
Expand Down
6 changes: 3 additions & 3 deletions server/rest/src/main/resources/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@
<meta content="noindex" name="robots"/>
<meta content="no-cache" http-equiv="cache-control"/>
<meta content="no-cache" http-equiv="pragma"/>
<link href="rest/favicon.png" rel="Shortcut Icon"/>
<link href="rest/css.css" rel="stylesheet"/>
<link href="favicon.png" rel="Shortcut Icon"/>
<link href="css.css" rel="stylesheet"/>
<title>Infinispan Server</title>
</head>
<body>
<p align="center">
<a href="https://www.infinispan.org"><img src="rest/banner.png" border="0"/></a>
<a href="https://www.infinispan.org"><img src="banner.png" border="0"/></a>
</p>
<div class="main">
<h1>Welcome to Infinispan Server</h1>
Expand Down