1818
1919package org .apache .cassandra .sidecar .handlers .livemigration ;
2020
21- import java .net . URLDecoder ;
22- import java .nio . charset . StandardCharsets ;
21+ import java .io . IOException ;
22+ import java .net . URI ;
2323import java .nio .file .FileSystems ;
2424import java .nio .file .Files ;
25+ import java .nio .file .NoSuchFileException ;
2526import java .nio .file .Path ;
2627import java .nio .file .PathMatcher ;
27- import java .nio .file .Paths ;
2828import java .util .ArrayList ;
2929import java .util .List ;
3030import java .util .Map ;
4242import io .vertx .core .net .SocketAddress ;
4343import io .vertx .ext .auth .authorization .Authorization ;
4444import io .vertx .ext .web .RoutingContext ;
45- import io .vertx .ext .web .handler .HttpException ;
4645import org .apache .cassandra .sidecar .acl .authorization .BasicPermissions ;
4746import org .apache .cassandra .sidecar .cluster .instance .InstanceMetadata ;
4847import org .apache .cassandra .sidecar .concurrent .ExecutorPools ;
5251import org .apache .cassandra .sidecar .handlers .AccessProtected ;
5352import org .apache .cassandra .sidecar .handlers .FileStreamHandler ;
5453import org .apache .cassandra .sidecar .livemigration .LiveMigrationInstanceMetadataUtil ;
54+ import org .apache .cassandra .sidecar .livemigration .LiveMigrationInstanceMetadataUtil .ResolvedPath ;
5555import org .apache .cassandra .sidecar .utils .CassandraInputValidator ;
5656import org .apache .cassandra .sidecar .utils .InstanceMetadataFetcher ;
5757import org .jetbrains .annotations .NotNull ;
6060import static org .apache .cassandra .sidecar .common .ApiEndpointsV1 .DIR_INDEX_PARAM ;
6161import static org .apache .cassandra .sidecar .common .ApiEndpointsV1 .DIR_TYPE_PARAM ;
6262import static org .apache .cassandra .sidecar .livemigration .LiveMigrationPlaceholderUtil .replacePlaceholder ;
63+ import static org .apache .cassandra .sidecar .utils .HttpExceptions .wrapHttpException ;
6364
6465/**
6566 * Handler that resolves and validates file paths for live migration operations.
@@ -94,7 +95,7 @@ protected Void extractParamsOrThrow(RoutingContext context)
9495 String dirType = context .pathParam (DIR_TYPE_PARAM );
9596 if (null == dirType || dirType .isEmpty () || null == LiveMigrationDirType .find (dirType ))
9697 {
97- throw new HttpException (HttpResponseStatus .BAD_REQUEST . code () , "Invalid directory type: " + dirType );
98+ throw wrapHttpException (HttpResponseStatus .BAD_REQUEST , "Invalid directory type: " + dirType );
9899 }
99100
100101 String dirIndex = context .pathParam (DIR_INDEX_PARAM );
@@ -105,11 +106,11 @@ protected Void extractParamsOrThrow(RoutingContext context)
105106 }
106107 catch (NumberFormatException formatException )
107108 {
108- throw new HttpException (HttpResponseStatus .BAD_REQUEST . code () , "Invalid directoryIndex: " + dirIndex );
109+ throw wrapHttpException (HttpResponseStatus .BAD_REQUEST , "Invalid directoryIndex: " + dirIndex );
109110 }
110111 if (index < 0 )
111112 {
112- throw new HttpException (HttpResponseStatus .BAD_REQUEST . code () , "Invalid directoryIndex: " + dirIndex );
113+ throw wrapHttpException (HttpResponseStatus .BAD_REQUEST , "Invalid directoryIndex: " + dirIndex );
113114 }
114115
115116 // Path params are not used further, hence returning null.
@@ -123,65 +124,91 @@ protected void handleInternal(RoutingContext rc,
123124 SocketAddress remoteAddress ,
124125 @ Nullable Void request )
125126 {
126- String reqPath = URLDecoder .decode (rc .request ().path (), StandardCharsets .UTF_8 );
127+ String reqPath ;
128+ try
129+ {
130+ reqPath = URI .create (rc .request ().path ()).getPath ();
131+ }
132+ catch (IllegalArgumentException e )
133+ {
134+ rc .fail (wrapHttpException (HttpResponseStatus .BAD_REQUEST , "Malformed request path" , e ));
135+ return ;
136+ }
127137
128138 if (reqPath .contains ("/../" ) || reqPath .endsWith ("/.." ))
129139 {
130140 LOGGER .warn ("Tried to access file using relative path({}). Rejecting the request." , reqPath );
131- rc .response ().setStatusCode (HttpResponseStatus .BAD_REQUEST .code ()).end ();
141+ rc .fail (wrapHttpException (HttpResponseStatus .BAD_REQUEST ,
142+ "Tried to access file using relative path: " + reqPath ));
132143 return ;
133144 }
134145
135146 InstanceMetadata instanceMeta = metadataFetcher .instance (host );
136147 String normalizedPath = rc .normalizedPath ();
137- String localFile ;
138148
149+ ResolvedPath resolved ;
139150 try
140151 {
141- localFile = LiveMigrationInstanceMetadataUtil .localPath (normalizedPath , instanceMeta ). toString ( );
152+ resolved = LiveMigrationInstanceMetadataUtil .resolveLexically (normalizedPath , instanceMeta );
142153 }
143154 catch (IllegalArgumentException e )
144155 {
145- LOGGER .warn ("Invalid path" , e );
146- rc .response ().setStatusCode (HttpResponseStatus .NOT_FOUND .code ()).end ();
156+ rc .fail (wrapHttpException (HttpResponseStatus .BAD_REQUEST , e .getMessage (), e ));
147157 return ;
148158 }
149159
150- Path path = Paths .get (localFile );
151-
160+ // Only the filesystem-touching checks (verifyContainment, isDirectory, isExcluded) run on
161+ // the worker thread; the lexical resolve above is pure string work and stays on the event
162+ // loop. validate() throws HttpException on any failure, which flows through processFailure
163+ // -> context.fail() so the framework's failure handler renders a JSON error response.
164+ String localFile = resolved .resolvedPath ().toString ();
152165 executorPools .service ()
153- .executeBlocking (() -> isInvalidPath (rc , path , reqPath , instanceMeta ))
154- .onSuccess (invalid -> {
155- if (!invalid )
156- {
157- rc .put (FileStreamHandler .FILE_PATH_CONTEXT_KEY , localFile );
158- rc .next ();
159- }
160- });
166+ .executeBlocking (() -> {
167+ validate (resolved , instanceMeta );
168+ return localFile ;
169+ })
170+ .onSuccess (file -> {
171+ rc .put (FileStreamHandler .FILE_PATH_CONTEXT_KEY , file );
172+ rc .next ();
173+ })
174+ .onFailure (cause -> processFailure (cause , rc , host , remoteAddress , request ));
161175 }
162176
163- private boolean isInvalidPath ( RoutingContext rc , Path path , String reqPath , InstanceMetadata instanceMeta )
177+ private void validate ( ResolvedPath resolved , InstanceMetadata instanceMeta )
164178 {
165- if (!Files .exists (path ))
179+ Path path = resolved .resolvedPath ();
180+ try
181+ {
182+ resolved .verifyContainment ();
183+ }
184+ catch (NoSuchFileException e )
166185 {
167186 LOGGER .info ("Requested file is not found. file={}" , path );
168- rc .response ().setStatusCode (HttpResponseStatus .NOT_FOUND .code ()).end ();
169- return true ;
187+ throw wrapHttpException (HttpResponseStatus .NOT_FOUND , "File not found" , e );
188+ }
189+ catch (IllegalArgumentException e )
190+ {
191+ throw wrapHttpException (HttpResponseStatus .FORBIDDEN , e .getMessage (), e );
170192 }
193+ catch (IOException e )
194+ {
195+ LOGGER .error ("Filesystem error while resolving {}" , path , e );
196+ throw wrapHttpException (HttpResponseStatus .INTERNAL_SERVER_ERROR ,
197+ "Filesystem error while resolving requested path" , e );
198+ }
199+
171200 if (Files .isDirectory (path ))
172201 {
173- LOGGER .info ("Cannot transfer directory. path={}." , reqPath );
174- rc .response ().setStatusCode (HttpResponseStatus .BAD_REQUEST .code ()).end ();
175- return true ;
202+ LOGGER .info ("Cannot transfer directory. path={}" , path );
203+ throw wrapHttpException (HttpResponseStatus .BAD_REQUEST , "Cannot transfer directory" );
176204 }
177205 if (isExcluded (path , instanceMeta ))
178206 {
179207 LOGGER .debug ("Requested path or one of its parent directories is excluded from Live Migration. " +
180- "path={}" , reqPath );
181- rc . response (). setStatusCode ( HttpResponseStatus .NOT_FOUND . code ()). end ();
182- return true ;
208+ "path={}" , path );
209+ throw wrapHttpException ( HttpResponseStatus .NOT_FOUND ,
210+ "Requested path is excluded from live migration" ) ;
183211 }
184- return false ;
185212 }
186213
187214 private boolean isExcluded (Path localFile , InstanceMetadata instanceMetadata )
0 commit comments