nvharikrishna commented on code in PR #215: URL: https://github.com/apache/cassandra-sidecar/pull/215#discussion_r2162101093
########## client-common/src/main/java/org/apache/cassandra/sidecar/common/ApiEndpointsV1.java: ########## @@ -147,14 +147,10 @@ public final class ApiEndpointsV1 public static final String LIVE_MIGRATION_FILES_API = LIVE_MIGRATION_API_PREFIX + "/files"; - public static final String LIVE_MIGRATION_CDC_RAW_DIR_PATH = LIVE_MIGRATION_FILES_API + "/cdc_raw"; - public static final String LIVE_MIGRATION_COMMITLOG_DIR_PATH = LIVE_MIGRATION_FILES_API + "/commitlog"; - public static final String LIVE_MIGRATION_DATA_FILE_DIR_PATH = LIVE_MIGRATION_FILES_API + "/data"; - public static final String LIVE_MIGRATION_HINTS_DIR_PATH = LIVE_MIGRATION_FILES_API + "/hints"; - public static final String LIVE_MIGRATION_LOCAL_SYSTEM_DATA_FILE_DIR_PATH = LIVE_MIGRATION_FILES_API - + "/local_system_data"; - public static final String LIVE_MIGRATION_SAVED_CACHES_DIR_PATH = LIVE_MIGRATION_FILES_API + "/saved_caches"; - + public static final String DIR_TYPE_PARAM = "dirType"; + public static final String DIR_INDEX_PARAM = "dirIndex"; + public static final String LIVE_MIGRATION_FILE_TRANSFER_API = LIVE_MIGRATION_FILES_API + "/:" + DIR_TYPE_PARAM + + "/:" + DIR_INDEX_PARAM + "/*"; Review Comment: done ########## server/src/main/java/org/apache/cassandra/sidecar/handlers/livemigration/LiveMigrationDirType.java: ########## @@ -0,0 +1,51 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.cassandra.sidecar.handlers.livemigration; + +/** + * Enum for holding different type of directories handled by Live Migration. + */ +public enum LiveMigrationDirType +{ + CDC_RAW_DIR("cdc_raw"), + COMMIT_LOG_DIR("commitlog"), + DATA_FIlE_DIR("data"), + HINTS_DIR("hints"), + LOCAL_SYSTEM_DATA_FILE_DIR("local_system_data"), + SAVED_CACHES_DIR("saved_caches"); + + public final String dirType; + + LiveMigrationDirType(String dirType) + { + this.dirType = dirType; + } + + public static LiveMigrationDirType find(String dirType) + { + for (LiveMigrationDirType type : values()) + { + if (type.dirType.equals(dirType)) + { + return type; + } + } + return null; + } Review Comment: done ########## server/src/main/java/org/apache/cassandra/sidecar/handlers/livemigration/LiveMigrationFileStreamHandler.java: ########## @@ -0,0 +1,243 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.cassandra.sidecar.handlers.livemigration; + +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.PathMatcher; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.inject.Inject; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.vertx.core.http.HttpServerRequest; +import io.vertx.core.net.SocketAddress; +import io.vertx.ext.auth.authorization.Authorization; +import io.vertx.ext.web.RoutingContext; +import io.vertx.ext.web.handler.HttpException; +import org.apache.cassandra.sidecar.acl.authorization.BasicPermissions; +import org.apache.cassandra.sidecar.cluster.instance.InstanceMetadata; +import org.apache.cassandra.sidecar.concurrent.ExecutorPools; +import org.apache.cassandra.sidecar.config.LiveMigrationConfiguration; +import org.apache.cassandra.sidecar.config.SidecarConfiguration; +import org.apache.cassandra.sidecar.handlers.AbstractHandler; +import org.apache.cassandra.sidecar.handlers.AccessProtected; +import org.apache.cassandra.sidecar.handlers.FileStreamHandler; +import org.apache.cassandra.sidecar.livemigration.LiveMigrationInstanceMetadataUtil; +import org.apache.cassandra.sidecar.utils.CassandraInputValidator; +import org.apache.cassandra.sidecar.utils.InstanceMetadataFetcher; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import static org.apache.cassandra.sidecar.common.ApiEndpointsV1.DIR_INDEX_PARAM; +import static org.apache.cassandra.sidecar.common.ApiEndpointsV1.DIR_TYPE_PARAM; +import static org.apache.cassandra.sidecar.livemigration.LiveMigrationPlaceholderUtil.replacePlaceholder; + +/** + * Handler that allows Cassandra instance files to be downloaded during LiveMigration. This handler + * doesn't stream the file but relies on {@link FileStreamHandler} to do so. This handler doesn't allow + * using "/.." in the path to access files. This handler does not serve files which are excluded in + * Live Migration configuration. + */ +public class LiveMigrationFileStreamHandler extends AbstractHandler<Void> implements AccessProtected +{ + + private static final Logger LOGGER = LoggerFactory.getLogger(LiveMigrationFileStreamHandler.class); + + private final Map<Integer, List<PathMatcher>> fileExclusionsByInstanceId = new ConcurrentHashMap<>(); + private final Map<Integer, List<PathMatcher>> dirExclusionsByInstanceId = new ConcurrentHashMap<>(); + private final LiveMigrationConfiguration liveMigrationConfiguration; + + @Inject + public LiveMigrationFileStreamHandler(InstanceMetadataFetcher metadataFetcher, + ExecutorPools executorPools, + CassandraInputValidator validator, + SidecarConfiguration sidecarConfiguration) + { + super(metadataFetcher, executorPools, validator); + this.liveMigrationConfiguration = sidecarConfiguration.liveMigrationConfiguration(); + } + + @Override + protected Void extractParamsOrThrow(RoutingContext context) + { + String dirType = context.pathParam(DIR_TYPE_PARAM); + if (null == dirType || dirType.isEmpty() || null == LiveMigrationDirType.find(dirType)) + { + throw new HttpException(HttpResponseStatus.BAD_REQUEST.code(), "Invalid directory type: " + dirType); + } + + String dirIndex = context.pathParam(DIR_INDEX_PARAM); + int index; + try + { + index = Integer.parseInt(dirIndex); + } + catch (NumberFormatException formatException) + { + throw new HttpException(HttpResponseStatus.BAD_REQUEST.code(), "Invalid directoryIndex: " + dirIndex); + } + if (index < 0) + { + throw new HttpException(HttpResponseStatus.BAD_REQUEST.code(), "Invalid directoryIndex: " + dirIndex); + } + + // Path params are not used further, hence returning null. + return null; + } + + @Override + protected void handleInternal(RoutingContext rc, + HttpServerRequest httpRequest, + @NotNull String host, + SocketAddress remoteAddress, + @Nullable Void request) + { + String reqPath = URLDecoder.decode(rc.request().path(), StandardCharsets.UTF_8); + + if (reqPath.contains("/../") || reqPath.endsWith("/..")) + { + LOGGER.warn("Tried to access file using relative path({}). Rejecting the request.", reqPath); + rc.response().setStatusCode(HttpResponseStatus.BAD_REQUEST.code()).end(); + return; + } Review Comment: request path can look like this `/api/v1/live-migration/files/data/0/ks/tb-1234/../../secrets`, but normalized path will not have `..`. [[more](https://vertx.io/docs/4.0.3/apidocs/io/vertx/ext/web/RoutingContext.html#normalizedPath--)] ########## server/src/main/java/org/apache/cassandra/sidecar/handlers/livemigration/LiveMigrationFileStreamHandler.java: ########## @@ -0,0 +1,243 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.cassandra.sidecar.handlers.livemigration; + +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.PathMatcher; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.inject.Inject; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.vertx.core.http.HttpServerRequest; +import io.vertx.core.net.SocketAddress; +import io.vertx.ext.auth.authorization.Authorization; +import io.vertx.ext.web.RoutingContext; +import io.vertx.ext.web.handler.HttpException; +import org.apache.cassandra.sidecar.acl.authorization.BasicPermissions; +import org.apache.cassandra.sidecar.cluster.instance.InstanceMetadata; +import org.apache.cassandra.sidecar.concurrent.ExecutorPools; +import org.apache.cassandra.sidecar.config.LiveMigrationConfiguration; +import org.apache.cassandra.sidecar.config.SidecarConfiguration; +import org.apache.cassandra.sidecar.handlers.AbstractHandler; +import org.apache.cassandra.sidecar.handlers.AccessProtected; +import org.apache.cassandra.sidecar.handlers.FileStreamHandler; +import org.apache.cassandra.sidecar.livemigration.LiveMigrationInstanceMetadataUtil; +import org.apache.cassandra.sidecar.utils.CassandraInputValidator; +import org.apache.cassandra.sidecar.utils.InstanceMetadataFetcher; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import static org.apache.cassandra.sidecar.common.ApiEndpointsV1.DIR_INDEX_PARAM; +import static org.apache.cassandra.sidecar.common.ApiEndpointsV1.DIR_TYPE_PARAM; +import static org.apache.cassandra.sidecar.livemigration.LiveMigrationPlaceholderUtil.replacePlaceholder; + +/** + * Handler that allows Cassandra instance files to be downloaded during LiveMigration. This handler + * doesn't stream the file but relies on {@link FileStreamHandler} to do so. This handler doesn't allow + * using "/.." in the path to access files. This handler does not serve files which are excluded in + * Live Migration configuration. + */ +public class LiveMigrationFileStreamHandler extends AbstractHandler<Void> implements AccessProtected +{ + + private static final Logger LOGGER = LoggerFactory.getLogger(LiveMigrationFileStreamHandler.class); + + private final Map<Integer, List<PathMatcher>> fileExclusionsByInstanceId = new ConcurrentHashMap<>(); + private final Map<Integer, List<PathMatcher>> dirExclusionsByInstanceId = new ConcurrentHashMap<>(); + private final LiveMigrationConfiguration liveMigrationConfiguration; + + @Inject + public LiveMigrationFileStreamHandler(InstanceMetadataFetcher metadataFetcher, + ExecutorPools executorPools, + CassandraInputValidator validator, + SidecarConfiguration sidecarConfiguration) + { + super(metadataFetcher, executorPools, validator); + this.liveMigrationConfiguration = sidecarConfiguration.liveMigrationConfiguration(); + } + + @Override + protected Void extractParamsOrThrow(RoutingContext context) + { + String dirType = context.pathParam(DIR_TYPE_PARAM); + if (null == dirType || dirType.isEmpty() || null == LiveMigrationDirType.find(dirType)) + { + throw new HttpException(HttpResponseStatus.BAD_REQUEST.code(), "Invalid directory type: " + dirType); + } + + String dirIndex = context.pathParam(DIR_INDEX_PARAM); + int index; + try + { + index = Integer.parseInt(dirIndex); + } + catch (NumberFormatException formatException) + { + throw new HttpException(HttpResponseStatus.BAD_REQUEST.code(), "Invalid directoryIndex: " + dirIndex); + } + if (index < 0) + { + throw new HttpException(HttpResponseStatus.BAD_REQUEST.code(), "Invalid directoryIndex: " + dirIndex); + } + + // Path params are not used further, hence returning null. + return null; + } + + @Override + protected void handleInternal(RoutingContext rc, + HttpServerRequest httpRequest, + @NotNull String host, + SocketAddress remoteAddress, + @Nullable Void request) + { + String reqPath = URLDecoder.decode(rc.request().path(), StandardCharsets.UTF_8); + + if (reqPath.contains("/../") || reqPath.endsWith("/..")) + { + LOGGER.warn("Tried to access file using relative path({}). Rejecting the request.", reqPath); + rc.response().setStatusCode(HttpResponseStatus.BAD_REQUEST.code()).end(); + return; + } + + InstanceMetadata instanceMeta = metadataFetcher.instance(host); + String normalizedPath = rc.normalizedPath(); + String localFile; + + try + { + localFile = LiveMigrationInstanceMetadataUtil.localPath(normalizedPath, instanceMeta); + } + catch (IllegalArgumentException e) + { + LOGGER.warn("Invalid path", e); + rc.response().setStatusCode(HttpResponseStatus.NOT_FOUND.code()).end(); + return; + } + + Path path = Paths.get(localFile); + + if (!Files.exists(path)) + { + LOGGER.info("File {} not found.", localFile); + rc.response().setStatusCode(HttpResponseStatus.NOT_FOUND.code()).end(); + } + if (Files.isDirectory(path)) + { + LOGGER.info("Cannot transfer directory for request path: {}.", reqPath); + rc.response().setStatusCode(HttpResponseStatus.BAD_REQUEST.code()).end(); + return; + } + if (isExcluded(path, instanceMeta)) + { + LOGGER.debug("Requested file {} or one of its parent directories is excluded from Live Migration.", reqPath); + rc.response().setStatusCode(HttpResponseStatus.NOT_FOUND.code()).end(); + return; + } + + rc.put(FileStreamHandler.FILE_PATH_CONTEXT_KEY, localFile); + rc.next(); Review Comment: done ########## server/src/main/java/org/apache/cassandra/sidecar/handlers/livemigration/LiveMigrationFileStreamHandler.java: ########## @@ -0,0 +1,243 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.cassandra.sidecar.handlers.livemigration; + +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.PathMatcher; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.inject.Inject; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.vertx.core.http.HttpServerRequest; +import io.vertx.core.net.SocketAddress; +import io.vertx.ext.auth.authorization.Authorization; +import io.vertx.ext.web.RoutingContext; +import io.vertx.ext.web.handler.HttpException; +import org.apache.cassandra.sidecar.acl.authorization.BasicPermissions; +import org.apache.cassandra.sidecar.cluster.instance.InstanceMetadata; +import org.apache.cassandra.sidecar.concurrent.ExecutorPools; +import org.apache.cassandra.sidecar.config.LiveMigrationConfiguration; +import org.apache.cassandra.sidecar.config.SidecarConfiguration; +import org.apache.cassandra.sidecar.handlers.AbstractHandler; +import org.apache.cassandra.sidecar.handlers.AccessProtected; +import org.apache.cassandra.sidecar.handlers.FileStreamHandler; +import org.apache.cassandra.sidecar.livemigration.LiveMigrationInstanceMetadataUtil; +import org.apache.cassandra.sidecar.utils.CassandraInputValidator; +import org.apache.cassandra.sidecar.utils.InstanceMetadataFetcher; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import static org.apache.cassandra.sidecar.common.ApiEndpointsV1.DIR_INDEX_PARAM; +import static org.apache.cassandra.sidecar.common.ApiEndpointsV1.DIR_TYPE_PARAM; +import static org.apache.cassandra.sidecar.livemigration.LiveMigrationPlaceholderUtil.replacePlaceholder; + +/** + * Handler that allows Cassandra instance files to be downloaded during LiveMigration. This handler + * doesn't stream the file but relies on {@link FileStreamHandler} to do so. This handler doesn't allow + * using "/.." in the path to access files. This handler does not serve files which are excluded in + * Live Migration configuration. + */ +public class LiveMigrationFileStreamHandler extends AbstractHandler<Void> implements AccessProtected +{ + + private static final Logger LOGGER = LoggerFactory.getLogger(LiveMigrationFileStreamHandler.class); + + private final Map<Integer, List<PathMatcher>> fileExclusionsByInstanceId = new ConcurrentHashMap<>(); + private final Map<Integer, List<PathMatcher>> dirExclusionsByInstanceId = new ConcurrentHashMap<>(); + private final LiveMigrationConfiguration liveMigrationConfiguration; + + @Inject + public LiveMigrationFileStreamHandler(InstanceMetadataFetcher metadataFetcher, + ExecutorPools executorPools, + CassandraInputValidator validator, + SidecarConfiguration sidecarConfiguration) + { + super(metadataFetcher, executorPools, validator); + this.liveMigrationConfiguration = sidecarConfiguration.liveMigrationConfiguration(); + } + + @Override + protected Void extractParamsOrThrow(RoutingContext context) Review Comment: right, similar to any other handler extending `AbstractHandler<Void>` it uses the arguments provided as part of 'handleInternal' method. ########## server/src/main/java/org/apache/cassandra/sidecar/handlers/livemigration/LiveMigrationFileStreamHandler.java: ########## @@ -0,0 +1,243 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.cassandra.sidecar.handlers.livemigration; + +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.PathMatcher; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.inject.Inject; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.vertx.core.http.HttpServerRequest; +import io.vertx.core.net.SocketAddress; +import io.vertx.ext.auth.authorization.Authorization; +import io.vertx.ext.web.RoutingContext; +import io.vertx.ext.web.handler.HttpException; +import org.apache.cassandra.sidecar.acl.authorization.BasicPermissions; +import org.apache.cassandra.sidecar.cluster.instance.InstanceMetadata; +import org.apache.cassandra.sidecar.concurrent.ExecutorPools; +import org.apache.cassandra.sidecar.config.LiveMigrationConfiguration; +import org.apache.cassandra.sidecar.config.SidecarConfiguration; +import org.apache.cassandra.sidecar.handlers.AbstractHandler; +import org.apache.cassandra.sidecar.handlers.AccessProtected; +import org.apache.cassandra.sidecar.handlers.FileStreamHandler; +import org.apache.cassandra.sidecar.livemigration.LiveMigrationInstanceMetadataUtil; +import org.apache.cassandra.sidecar.utils.CassandraInputValidator; +import org.apache.cassandra.sidecar.utils.InstanceMetadataFetcher; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import static org.apache.cassandra.sidecar.common.ApiEndpointsV1.DIR_INDEX_PARAM; +import static org.apache.cassandra.sidecar.common.ApiEndpointsV1.DIR_TYPE_PARAM; +import static org.apache.cassandra.sidecar.livemigration.LiveMigrationPlaceholderUtil.replacePlaceholder; + +/** + * Handler that allows Cassandra instance files to be downloaded during LiveMigration. This handler + * doesn't stream the file but relies on {@link FileStreamHandler} to do so. This handler doesn't allow + * using "/.." in the path to access files. This handler does not serve files which are excluded in + * Live Migration configuration. + */ +public class LiveMigrationFileStreamHandler extends AbstractHandler<Void> implements AccessProtected +{ + + private static final Logger LOGGER = LoggerFactory.getLogger(LiveMigrationFileStreamHandler.class); + + private final Map<Integer, List<PathMatcher>> fileExclusionsByInstanceId = new ConcurrentHashMap<>(); + private final Map<Integer, List<PathMatcher>> dirExclusionsByInstanceId = new ConcurrentHashMap<>(); + private final LiveMigrationConfiguration liveMigrationConfiguration; + + @Inject + public LiveMigrationFileStreamHandler(InstanceMetadataFetcher metadataFetcher, + ExecutorPools executorPools, + CassandraInputValidator validator, + SidecarConfiguration sidecarConfiguration) + { + super(metadataFetcher, executorPools, validator); + this.liveMigrationConfiguration = sidecarConfiguration.liveMigrationConfiguration(); + } + + @Override + protected Void extractParamsOrThrow(RoutingContext context) + { + String dirType = context.pathParam(DIR_TYPE_PARAM); + if (null == dirType || dirType.isEmpty() || null == LiveMigrationDirType.find(dirType)) + { + throw new HttpException(HttpResponseStatus.BAD_REQUEST.code(), "Invalid directory type: " + dirType); + } + + String dirIndex = context.pathParam(DIR_INDEX_PARAM); + int index; + try + { + index = Integer.parseInt(dirIndex); + } + catch (NumberFormatException formatException) + { + throw new HttpException(HttpResponseStatus.BAD_REQUEST.code(), "Invalid directoryIndex: " + dirIndex); + } + if (index < 0) + { + throw new HttpException(HttpResponseStatus.BAD_REQUEST.code(), "Invalid directoryIndex: " + dirIndex); + } + + // Path params are not used further, hence returning null. + return null; + } + + @Override + protected void handleInternal(RoutingContext rc, + HttpServerRequest httpRequest, + @NotNull String host, + SocketAddress remoteAddress, + @Nullable Void request) + { + String reqPath = URLDecoder.decode(rc.request().path(), StandardCharsets.UTF_8); + + if (reqPath.contains("/../") || reqPath.endsWith("/..")) + { + LOGGER.warn("Tried to access file using relative path({}). Rejecting the request.", reqPath); + rc.response().setStatusCode(HttpResponseStatus.BAD_REQUEST.code()).end(); + return; + } + + InstanceMetadata instanceMeta = metadataFetcher.instance(host); + String normalizedPath = rc.normalizedPath(); + String localFile; + + try + { + localFile = LiveMigrationInstanceMetadataUtil.localPath(normalizedPath, instanceMeta); + } + catch (IllegalArgumentException e) + { + LOGGER.warn("Invalid path", e); + rc.response().setStatusCode(HttpResponseStatus.NOT_FOUND.code()).end(); + return; + } + + Path path = Paths.get(localFile); + + if (!Files.exists(path)) + { + LOGGER.info("File {} not found.", localFile); Review Comment: done ########## server/src/main/java/org/apache/cassandra/sidecar/handlers/livemigration/LiveMigrationFileStreamHandler.java: ########## @@ -0,0 +1,243 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.cassandra.sidecar.handlers.livemigration; + +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.PathMatcher; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.inject.Inject; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.vertx.core.http.HttpServerRequest; +import io.vertx.core.net.SocketAddress; +import io.vertx.ext.auth.authorization.Authorization; +import io.vertx.ext.web.RoutingContext; +import io.vertx.ext.web.handler.HttpException; +import org.apache.cassandra.sidecar.acl.authorization.BasicPermissions; +import org.apache.cassandra.sidecar.cluster.instance.InstanceMetadata; +import org.apache.cassandra.sidecar.concurrent.ExecutorPools; +import org.apache.cassandra.sidecar.config.LiveMigrationConfiguration; +import org.apache.cassandra.sidecar.config.SidecarConfiguration; +import org.apache.cassandra.sidecar.handlers.AbstractHandler; +import org.apache.cassandra.sidecar.handlers.AccessProtected; +import org.apache.cassandra.sidecar.handlers.FileStreamHandler; +import org.apache.cassandra.sidecar.livemigration.LiveMigrationInstanceMetadataUtil; +import org.apache.cassandra.sidecar.utils.CassandraInputValidator; +import org.apache.cassandra.sidecar.utils.InstanceMetadataFetcher; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import static org.apache.cassandra.sidecar.common.ApiEndpointsV1.DIR_INDEX_PARAM; +import static org.apache.cassandra.sidecar.common.ApiEndpointsV1.DIR_TYPE_PARAM; +import static org.apache.cassandra.sidecar.livemigration.LiveMigrationPlaceholderUtil.replacePlaceholder; + +/** + * Handler that allows Cassandra instance files to be downloaded during LiveMigration. This handler + * doesn't stream the file but relies on {@link FileStreamHandler} to do so. This handler doesn't allow + * using "/.." in the path to access files. This handler does not serve files which are excluded in + * Live Migration configuration. + */ +public class LiveMigrationFileStreamHandler extends AbstractHandler<Void> implements AccessProtected +{ + + private static final Logger LOGGER = LoggerFactory.getLogger(LiveMigrationFileStreamHandler.class); + + private final Map<Integer, List<PathMatcher>> fileExclusionsByInstanceId = new ConcurrentHashMap<>(); + private final Map<Integer, List<PathMatcher>> dirExclusionsByInstanceId = new ConcurrentHashMap<>(); + private final LiveMigrationConfiguration liveMigrationConfiguration; + + @Inject + public LiveMigrationFileStreamHandler(InstanceMetadataFetcher metadataFetcher, + ExecutorPools executorPools, + CassandraInputValidator validator, + SidecarConfiguration sidecarConfiguration) + { + super(metadataFetcher, executorPools, validator); + this.liveMigrationConfiguration = sidecarConfiguration.liveMigrationConfiguration(); + } + + @Override + protected Void extractParamsOrThrow(RoutingContext context) + { + String dirType = context.pathParam(DIR_TYPE_PARAM); + if (null == dirType || dirType.isEmpty() || null == LiveMigrationDirType.find(dirType)) + { + throw new HttpException(HttpResponseStatus.BAD_REQUEST.code(), "Invalid directory type: " + dirType); + } + + String dirIndex = context.pathParam(DIR_INDEX_PARAM); + int index; + try + { + index = Integer.parseInt(dirIndex); + } + catch (NumberFormatException formatException) + { + throw new HttpException(HttpResponseStatus.BAD_REQUEST.code(), "Invalid directoryIndex: " + dirIndex); + } + if (index < 0) + { + throw new HttpException(HttpResponseStatus.BAD_REQUEST.code(), "Invalid directoryIndex: " + dirIndex); + } + + // Path params are not used further, hence returning null. + return null; + } + + @Override + protected void handleInternal(RoutingContext rc, + HttpServerRequest httpRequest, + @NotNull String host, + SocketAddress remoteAddress, + @Nullable Void request) + { + String reqPath = URLDecoder.decode(rc.request().path(), StandardCharsets.UTF_8); + + if (reqPath.contains("/../") || reqPath.endsWith("/..")) + { + LOGGER.warn("Tried to access file using relative path({}). Rejecting the request.", reqPath); + rc.response().setStatusCode(HttpResponseStatus.BAD_REQUEST.code()).end(); + return; + } + + InstanceMetadata instanceMeta = metadataFetcher.instance(host); + String normalizedPath = rc.normalizedPath(); + String localFile; + + try + { + localFile = LiveMigrationInstanceMetadataUtil.localPath(normalizedPath, instanceMeta); + } + catch (IllegalArgumentException e) + { + LOGGER.warn("Invalid path", e); + rc.response().setStatusCode(HttpResponseStatus.NOT_FOUND.code()).end(); + return; + } + + Path path = Paths.get(localFile); + + if (!Files.exists(path)) + { + LOGGER.info("File {} not found.", localFile); + rc.response().setStatusCode(HttpResponseStatus.NOT_FOUND.code()).end(); Review Comment: done -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: pr-unsubscr...@cassandra.apache.org For queries about this service, please contact Infrastructure at: us...@infra.apache.org --------------------------------------------------------------------- To unsubscribe, e-mail: pr-unsubscr...@cassandra.apache.org For additional commands, e-mail: pr-h...@cassandra.apache.org