From 1a82d8a779871cd395df5baa4dec453a109c7290 Mon Sep 17 00:00:00 2001 From: vga91 Date: Mon, 28 Jul 2025 20:43:37 +0200 Subject: [PATCH 1/2] Various changes for Neo4j 2025.x --- README.md | 6 +- docs/modules/ROOT/nav.adoc | 3 +- .../ROOT/pages/appendix_migration.adoc | 3 +- .../ROOT/pages/appendix_migration2025.adoc | 39 ++++++ docs/modules/ROOT/pages/export.adoc | 2 +- docs/modules/ROOT/pages/index.adoc | 3 +- docs/modules/ROOT/pages/install.adoc | 6 +- docs/modules/ROOT/pages/mapping.adoc | 3 +- pom.xml | 31 ++--- src/main/java/n10s/CommonProcedures.java | 3 + src/main/java/n10s/endpoint/RDFEndpoint.java | 26 +++- src/test/java/n10s/RDFProceduresTest.java | 6 +- .../java/n10s/endpoint/RDFEndpointTest.java | 123 ++++++++++++------ 13 files changed, 184 insertions(+), 70 deletions(-) create mode 100644 docs/modules/ROOT/pages/appendix_migration2025.adoc diff --git a/README.md b/README.md index a7422b0a..dfd04ac4 100644 --- a/README.md +++ b/README.md @@ -49,8 +49,10 @@ it in the /plugins directory of your Neo4j instance. ### Verifying the installation + You can check that the installation went well by: -Running `call dbms.procedures()`. The list of procedures should include a number of them prefixed by **n10s**. +Running `SHOW PROCEDURES WHERE name STARTS WITH "n10s"`. +The list of procedures should include a number of them prefixed by **n10s**. If you installed the http endpoint, you can check it was correctly installed by looking in the logs and making sure they show the following line on startup: @@ -102,7 +104,7 @@ call n10s.graphconfig.init( { handleMultival: "ARRAY", #### 2. Importing RDF data -Once the Graph config is created we can import data from a url using `fetch`: +Once the Graph config is created we can import data from an url using `fetch`: ``` call n10s.rdf.import.fetch( "https://raw.githubusercontent.com/jbarrasa/neosemantics/3.5/docs/rdf/nsmntx.ttl", diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc index 21ce0887..25f05a60 100644 --- a/docs/modules/ROOT/nav.adoc +++ b/docs/modules/ROOT/nav.adoc @@ -45,6 +45,7 @@ // ** xref:reference.adoc#_utility_functions[12.2. Utility Functions] // ** xref:reference.adoc#_extensions_http_endpoints[12.3. Extensions (HTTP endpoints)] * xref:examples.adoc[13. Projects using Neosemantics] -* xref:appendix_migration.adoc[A. Migrating from neosemantics 3 to 4] +** xref:appendix_migration2025.adoc[A. Migrating from neosemantics 4.x and 5.x to 2025.x] +* xref:appendix_migration.adoc[B. Migrating from neosemantics 3 to 4] // ** xref:appendix_migration.adoc#_who_should_read_this_guide[A.1. Who should read this guide] // ** xref:appendix_migration.adoc#_changes_in_neosemantics_4_x[A.2. Changes in neosemantics 4.x] diff --git a/docs/modules/ROOT/pages/appendix_migration.adoc b/docs/modules/ROOT/pages/appendix_migration.adoc index 1dbfe0df..e929dce0 100644 --- a/docs/modules/ROOT/pages/appendix_migration.adoc +++ b/docs/modules/ROOT/pages/appendix_migration.adoc @@ -1,4 +1,4 @@ -= Appendix A. Migrating from neosemantics 3 to 4 += Appendix B. Migrating from neosemantics 3 to 4 :page-pagination: [appendix] @@ -11,6 +11,7 @@ If you have previously used neosemantics v3.x, you can find the information you This documentation is intended for users who are familiar with neosemantics. Based on this assumption, we are intentionally brief in the examples and comparisons. + === Changes in neosemantics 4.x Changes are grouped in two categories: diff --git a/docs/modules/ROOT/pages/appendix_migration2025.adoc b/docs/modules/ROOT/pages/appendix_migration2025.adoc new file mode 100644 index 00000000..593bd2de --- /dev/null +++ b/docs/modules/ROOT/pages/appendix_migration2025.adoc @@ -0,0 +1,39 @@ += Appendix A. Migrating to 2025.x + + +:page-pagination: + +[abstract] +If you have previously used neosemantics up to v5.x, you can find the information you will need to migrate to using neosemantics v2025.x. + +== Who should read this guide + +This documentation is intended for users who are familiar with neosemantics. Based on this assumption, we are intentionally brief in the examples and comparisons. + +=== Changes in neosemantics 2025.x + +==== Changes to the URL structure of the RDF endpoint + +Starting with Neo4j versions that use recent versions of the Jetty web server (like Jetty 12 in Neo4j 2025.x), the structure for URLs sent to the RDF description endpoint has become stricter. Passing a full URI containing encoded slashes (`%2F`) directly within the URL path is no longer permitted and will result in an `HTTP 400 Bad Request` error. + +This change is a direct consequence of enhanced security policies in Jetty. Specifically, the `UriCompliance` mode, which is stricter by default, now flags ambiguous path separators to prevent potential 'Path Traversal' vulnerabilities. For more technical details, see the Jetty issue https://github.com/jetty/jetty.project/issues/12162[#12162] and the `AMBIGUOUS_PATH_SEPARATOR` definition in the https://github.com/jetty/jetty.project/blob/jetty-12.0.x/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/UriCompliance.java#L75[UriCompliance source code]. + +The correct way to describe a resource identified by a URI is to pass it as a query parameter named `nodeidentifier`. +Note that the URI should be encoded as before. + +[options="header"] +|=== +| Previous Method (No longer supported) | Correct Method (Required) +| The full URI was part of the *path*. | The full URI is passed as the `nodeidentifier` *query parameter*. +| `.../describe/http%3A%2F%2Fneo4j.org%2Find%23neo4j355` | `.../describe?nodeidentifier=http%3A%2F%2Fneo4j.org%2Find%23neo4j355` +|=== + +.Request Transformation Example +[source,http] +---- +// OLD REQUEST (NOW RETURNS 400 ERROR) +GET /rdf/neo4j/describe/http%3A%2F%2Fneo4j.org%2Find%23neo4j355 + +// NEW REQUEST (CORRECT) +GET /rdf/neo4j/describe?nodeidentifier=http%3A%2F%2Fneo4j.org%2Find%23neo4j355 +---- \ No newline at end of file diff --git a/docs/modules/ROOT/pages/export.adoc b/docs/modules/ROOT/pages/export.adoc index 0c90a993..09009711 100644 --- a/docs/modules/ROOT/pages/export.adoc +++ b/docs/modules/ROOT/pages/export.adoc @@ -141,7 +141,7 @@ URIs need to be encoded in `GET` requests. [source,Cypher] ---- -:GET /rdf/neo4j/describe/http%3A%2F%2Fneo4j.org%2Find%23neo4j355?format=RDF/XML +:GET /rdf/neo4j/describe?nodeidentifier=http%3A%2F%2Fneo4j.org%2Find%23neo4j355?format=RDF/XML ---- Notice the URL encoding of the URI (the clean URI is `http://neo4j.org/ind#neo4j355`) and the `format` parameter to specify the serialisation format. Here's the output of the request: diff --git a/docs/modules/ROOT/pages/index.adoc b/docs/modules/ROOT/pages/index.adoc index 91f4f9a5..84d81043 100644 --- a/docs/modules/ROOT/pages/index.adoc +++ b/docs/modules/ROOT/pages/index.adoc @@ -19,6 +19,7 @@ The guide covers the following areas: * xref:inference.adoc[Chapter 11, Inferencing/Reasoning] — A detailed guide to inferencing and reasoning. * xref:reference.adoc[Chapter 12, Neosemantics Reference] — An overview of all procedures and functions in the library. * xref:examples.adoc[Chapter 13, Projects using Neosemantics] — A list of projects using n10s. -* xref:appendix_migration.adoc[Appendix A, Migrating from neosemantics 3 to 4] — A guide for neosemantics 3.x users +* xref:appendix_migration2025.adoc[Appendix A, Migrating from neosemantics 4.x and 5.x to 2025.x] — A guide for neosemantics 4.x and 5.x users +* xref:appendix_migration.adoc[Appendix B, Migrating from neosemantics 3 to 4] — A guide for neosemantics 3.x users image::nsmntx-block-diagram.png[Neosemantics diagram, 200,align="center"] \ No newline at end of file diff --git a/docs/modules/ROOT/pages/install.adoc b/docs/modules/ROOT/pages/install.adoc index 5178466a..785b6d81 100644 --- a/docs/modules/ROOT/pages/install.adoc +++ b/docs/modules/ROOT/pages/install.adoc @@ -4,7 +4,7 @@ You can either download a prebuilt jar from the https://github.com/jbarrasa/neosemantics/releases[releases area] or build it from the source. If you prefer to build, check the note below. -1. Copy the the jar(s) in the /plugins directory of your Neo4j instance. (**note:** If you're going to use the JSON-LD serialisation format for RDF, you'll need to include also link:/labs/apoc/[APOC]) +1. Copy the jar(s) in the /plugins directory of your Neo4j instance. (**note:** If you're going to use the JSON-LD serialisation format for RDF, you'll need to include also link:/labs/apoc/[APOC]) 2. Add the following line to your /conf/neo4j.conf (notice that it is possible to modify where the extension is mounted by using an alternative name to `/rdf` below). + [source,shell] @@ -19,7 +19,7 @@ When the property `dbms.security.procedures.allowlist` is set, then it must incl 3. Restart the server. 4. Check that the installation went well by running [source,cypher] -call dbms.procedures() +SHOW PROCEDURES WHERE name STARTS WITH "n10s" The list of procedures should include the ones documented below. You can check that the extension is mounted by running @@ -27,7 +27,7 @@ You can check that the extension is mounted by running ---- :GET http://localhost:7474/rdf/ping ---- -The previous command assumes you're running neo4j on your local machine, replace `localhos` with the host name if that is not the case. +The previous command assumes you're running neo4j on your local machine, replace `localhost` with the host name if that is not the case. **Note on build** diff --git a/docs/modules/ROOT/pages/mapping.adoc b/docs/modules/ROOT/pages/mapping.adoc index c2539081..93fb3766 100644 --- a/docs/modules/ROOT/pages/mapping.adoc +++ b/docs/modules/ROOT/pages/mapping.adoc @@ -126,8 +126,7 @@ Let's use the `/cypher` method to serialise as RDF an order given its `orderID`. [source,Cypher] ---- -:POST /rdf/cypher -{ "cypher" : "MATCH path = (n:Order { orderID : '10785'})-[:ORDERS]->()-[:PART_OF]->(:Category { categoryName : 'Beverages'}) RETURN path " , "format": "RDF/XML" , "mappedElemsOnly" : true } + ---- The Cypher query uses the elements in the Neo4j graph but the generated RDF uses schema.org vocabulary elements. The mapping we just defined is bridging the two. Note that the mapping is completely dynamic which means that any change to the mapping definition will be applied to any subsequent request. diff --git a/pom.xml b/pom.xml index 5f82d55c..e8ab7e2e 100644 --- a/pom.xml +++ b/pom.xml @@ -36,22 +36,23 @@ 2025.06.2 4.3.12 - 2.19.0 + 2.13.3 + UTF-8 - - - - - com.fasterxml.jackson - jackson-bom - ${jackson.version} - pom - import - - - + + + + + + + + + + + + @@ -249,8 +250,8 @@ maven-compiler-plugin 3.8.1 - 11 - 11 + 21 + 21 diff --git a/src/main/java/n10s/CommonProcedures.java b/src/main/java/n10s/CommonProcedures.java index 2c494a2d..8e8b5222 100644 --- a/src/main/java/n10s/CommonProcedures.java +++ b/src/main/java/n10s/CommonProcedures.java @@ -9,6 +9,7 @@ import java.net.URL; import java.net.URLConnection; import java.nio.charset.Charset; +import java.util.Arrays; import java.util.Iterator; import java.util.Map; import java.util.zip.GZIPInputStream; @@ -19,6 +20,7 @@ import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; import org.apache.commons.compress.compressors.bzip2.BZip2CompressorInputStream; import org.apache.commons.io.IOUtils; +import org.eclipse.rdf4j.common.lang.FileFormat; import org.eclipse.rdf4j.rio.RDFFormat; import org.eclipse.rdf4j.rio.RDFParser; import org.eclipse.rdf4j.rio.Rio; @@ -230,6 +232,7 @@ public static boolean isRedirect(HttpURLConnection con) throws IOException { protected RDFFormat getFormat(String format) throws RDFImportBadParams { + System.out.println("availableParsers = " + Arrays.stream(availableParsers).map(FileFormat::getName).toList()); if (format != null) { for (RDFFormat parser : availableParsers) { if (parser.getName().equals(format)) { diff --git a/src/main/java/n10s/endpoint/RDFEndpoint.java b/src/main/java/n10s/endpoint/RDFEndpoint.java index 77db4ece..66a1ab21 100644 --- a/src/main/java/n10s/endpoint/RDFEndpoint.java +++ b/src/main/java/n10s/endpoint/RDFEndpoint.java @@ -78,21 +78,41 @@ public Response ping() throws IOException { return Response.ok().entity(objectMapper.writeValueAsString(results)).build(); } + /* + HTTP error 400 (Bad Request) occurs because the slash (/) in your identifier, + once encoded in %2F by URLEncoder, + is often blocked for security reasons by the application server (such as Tomcat, WildFly) or by a proxy before it even reaches your JAX-RS code. + */ @GET - @Path("/{dbname}/describe/{nodeidentifier}") +// @Path("/{dbname}/describe/{nodeidentifier}") +// @Path("/{dbname}/describe/{nodeidentifier:.+}") + @Path("/{dbname}/describe") @Produces({"application/rdf+xml", "text/plain", "text/turtle", "text/n3", "application/trig", "application/ld+json", "application/n-quads", "text/x-turtlestar", "application/x-trigstar"}) public Response nodebyIdOrUri(@Context DatabaseManagementService gds, @PathParam("dbname") String dbNameParam, - @PathParam("nodeidentifier") String nodeIdentifier, + @QueryParam("nodeIdentifier") String nodeIdentifier, @QueryParam("graphuri") String namedGraphId, @QueryParam("excludeContext") String excludeContextParam, @QueryParam("mappedElemsOnly") String onlyMappedInfo, @QueryParam("format") String format, @HeaderParam("accept") String acceptHeaderParam) { return Response.ok().entity((StreamingOutput) outputStream -> { - +// @GET +// @Path("/{dbname}/describe/{nodeidentifier}") +// @Produces({"application/rdf+xml", "text/plain", "text/turtle", "text/n3", +// "application/trig", "application/ld+json", "application/n-quads", "text/x-turtlestar", +// "application/x-trigstar"}) +// public Response nodebyIdOrUri(@Context DatabaseManagementService gds, +// @PathParam("dbname") String dbNameParam, +// @PathParam("nodeidentifier") String nodeIdentifier, +// @QueryParam("graphuri") String namedGraphId, +// @QueryParam("excludeContext") String excludeContextParam, +// @QueryParam("mappedElemsOnly") String onlyMappedInfo, +// @QueryParam("format") String format, +// @HeaderParam("accept") String acceptHeaderParam) { +// return Response.ok().entity((StreamingOutput) outputStream -> { RDFWriter writer = startRdfWriter(getFormat(acceptHeaderParam, format), outputStream); GraphDatabaseService neo4j = gds.database(dbNameParam); try (Transaction tx = neo4j.beginTx()) { diff --git a/src/test/java/n10s/RDFProceduresTest.java b/src/test/java/n10s/RDFProceduresTest.java index 13ed1200..97a3c577 100644 --- a/src/test/java/n10s/RDFProceduresTest.java +++ b/src/test/java/n10s/RDFProceduresTest.java @@ -1,5 +1,6 @@ package n10s; +import static n10s.CommonProcedures.UNIQUENESS_CONSTRAINT_ON_URI; import static n10s.CommonProcedures.UNIQUENESS_CONSTRAINT_STATEMENT; import static n10s.graphconfig.Params.PREFIX_SEPARATOR; import static org.junit.Assert.assertArrayEquals; @@ -2833,10 +2834,9 @@ public void multivalMultitypeSamePartialTx() throws Exception { assertEquals(6, session.run("MATCH (n:Resource) RETURN count(n) as nodeCount ").next().get("nodeCount").asInt()); - Result result = session.run("MATCH (n:Resource) RETURN n.ns0__totalLength as tl "); + Result result = session.run("MATCH (n:Resource) WHERE n.uri = \"http://dbpedia.org/resource/%22No_Flashlight%22:_Songs_of_the_Fulfilled_Night\" RETURN n.ns0__totalLength as tl"); Record next = result.next(); assertTrue(next.get("tl").asList().containsAll(Arrays.asList("45.75^^xsd__double", "2271.0"))); //"2271.0^^ns1__second" if custom datatypes were being kept - assertEquals(1L, session.run("MATCH (n:Resource) WHERE '45.75^^xsd__double' in n.ns0__totalLength RETURN count(n) as ct ").next().get("ct").asLong()); assertTrue(session.run("MATCH (r:Resource) DETACH DELETE r RETURN count(r) as ct").next().get("ct").asLong() > 0); @@ -2866,9 +2866,7 @@ public void multivalMultitypeSamePartialTx() throws Exception { assertEquals(6, session.run("MATCH (n:Resource) RETURN count(n) as nodeCount ").next().get("nodeCount").asInt()); assertEquals(1L, session.run("MATCH (n:Resource) WHERE '45.75^^xsd__double' in n.ns0__totalLength RETURN count(n) as ct ").next().get("ct").asLong()); - } - } @Test diff --git a/src/test/java/n10s/endpoint/RDFEndpointTest.java b/src/test/java/n10s/endpoint/RDFEndpointTest.java index b6e7581b..34658382 100644 --- a/src/test/java/n10s/endpoint/RDFEndpointTest.java +++ b/src/test/java/n10s/endpoint/RDFEndpointTest.java @@ -153,7 +153,7 @@ public void testGetNodeById() throws Exception { // When HTTP.Response response = HTTP.withHeaders("Accept", "application/ld+json").GET( - resolveURI(neo4j.httpURI(), "neo4j/describe/" + id.toString())); + resolveURI(neo4j.httpURI(), "neo4j/describe?nodeIdentifier=" + id.toString())); String expected = String.format("[ {\n" + " \"@id\" : \"neo4j://graph.individuals%1$s\",\n" @@ -231,7 +231,7 @@ public void testGetNodeByIdFromRDFizedLPG() throws Exception { // When HTTP.Response response = HTTP.withHeaders("Accept", "application/ld+json").GET( - resolveURI(neo4j.httpURI(), "neo4j/describe/http%3A%2F%2Fneo4j.com%2Fmovies%2FKeanu")); + resolveURI(neo4j.httpURI(), "neo4j/describe?nodeIdentifier=http%3A%2F%2Fneo4j.com%2Fmovies%2FKeanu")); String expected = "{\n" + " \"@id\" : \"http://neo4j.com/movies/Keanu\",\n" + @@ -252,7 +252,7 @@ public void testGetNodeByIdFromRDFizedLPG() throws Exception { // When response = HTTP.withHeaders("Accept", "text/x-turtlestar").GET( - resolveURI(neo4j.httpURI(), "neo4j/describe/http%3A%2F%2Fneo4j.com%2Fmovies%2FHugo")); + resolveURI(neo4j.httpURI(), "neo4j/describe?nodeIdentifier=http%3A%2F%2Fneo4j.com%2Fmovies%2FHugo")); expected = "@prefix rdf: .\n" + "@prefix neovoc: .\n" + @@ -438,7 +438,8 @@ public void testPrefixwithHyphen() throws Exception { } Response response = HTTP.withHeaders("Accept", "text/plain").GET( - resolveURI(neo4j.httpURI(), "neo4j/describe/http%3A%2F%2Fwww.example.com%2Fexample%23Enitity1Individual?format=RDF/XML")); +// resolveURI(neo4j.httpURI(), "neo4j/describe?nodeIdentifier=http%3A%2F%2Fwww.example.com%2Fexample%23Enitity1Individual&format=RDF/XML")); + resolveURI(neo4j.httpURI(), "neo4j/describe?nodeIdentifier=" + URLEncoder.encode("http://www.example.com/example#Enitity1Individual", StandardCharsets.UTF_8) + "&format=RDF/XML")); String expected = " .\n" + @@ -590,7 +591,7 @@ public void testGetNodeByIdRDFStar() throws Exception { // When HTTP.Response response = HTTP.withHeaders("Accept", "text/x-turtlestar").GET( - resolveURI(neo4j.httpURI(), "neo4j/describe/") + resolveURI(neo4j.httpURI(), "neo4j/describe?nodeIdentifier=") + id3.toString()); String expected = String.format( "@prefix neoind: .\n" @@ -650,7 +651,7 @@ public void ImportGetNodeById() throws Exception { session.run("CALL n10s.graphconfig.init( { handleVocabUris: 'IGNORE', typesToLabels: true } )"); org.neo4j.driver.Result importResults = session.run("CALL n10s.rdf.import.fetch('" + - resolveURI(neo4j.httpURI(), "neo4j/describe/") + resolveURI(neo4j.httpURI(), "neo4j/describe?nodeIdentifier=") + id + "','Turtle')"); @@ -707,7 +708,7 @@ public void ImportGetNodeByIdOnImportedOnto() throws Exception { // then export elements and check the output is right HTTP.Response response = HTTP.withHeaders("Accept", "text/turtle").GET( - resolveURI(neo4j.httpURI(), "neo4j/describe/") + + resolveURI(neo4j.httpURI(), "neo4j/describe?nodeIdentifier=") + "http%3A%2F%2Fn4j.com%2Ftst1%2Fontologies%2F2017%2F4%2FCyber_EA_Smart_City%23RF_signal_strength"); String expected = "@prefix n4sch: .\n" + @@ -762,7 +763,7 @@ public void ImportGetNodeByUriOnImportedOntoShorten() throws Exception { } // then export elements and check the output is right HTTP.Response response = HTTP.withHeaders("Accept", "text/turtle").GET( - resolveURI(neo4j.httpURI(), "neo4j/describe/" + id)); + resolveURI(neo4j.httpURI(), "neo4j/describe?nodeIdentifier=" + id)); String expected = "@prefix n4sch: .\n" + "@prefix n4ind: .\n" + @@ -777,7 +778,7 @@ public void ImportGetNodeByUriOnImportedOntoShorten() throws Exception { response = HTTP.withHeaders("Accept", "text/turtle").GET( - resolveURI(neo4j.httpURI(), "neo4j/describe/") + + resolveURI(neo4j.httpURI(), "neo4j/describe?nodeIdentifier=") + URLEncoder.encode("http://n4j.com/tst1/ontologies/2017/4/Cyber_EA_Smart_City#RF_signal_strength", "UTF-8")); assertEquals(200, response.status()); @@ -818,8 +819,22 @@ public void ImportGetNodeByUriOnImportedOntoIgnore() throws Exception { // then export elements and check the output is right HTTP.Response response = HTTP.withHeaders("Accept", "text/turtle").GET( - resolveURI(neo4j.httpURI(), "neo4j/describe/" + id)); - + resolveURI(neo4j.httpURI(), "neo4j/describe?nodeIdentifier=" + id)); +// +// +// +//Error 400 Ambiguous URI path separator +// +// +//

HTTP ERROR 400 Ambiguous URI path separator

+// +// +// +// +//
URI:/badURI
STATUS:400
MESSAGE:Ambiguous URI path separator
+// +// +// String expected = "@prefix rdf: .\n" + "@prefix neovoc: .\n" + "@prefix neoind: .\n" + @@ -832,11 +847,45 @@ public void ImportGetNodeByUriOnImportedOntoIgnore() throws Exception { assertTrue(ModelTestUtils .compareModels(expected, RDFFormat.TURTLE, response.rawContent(), RDFFormat.TURTLE)); + /* TODO + Capisco perfettamente la tua frustrazione. È una situazione comune quando si aggiorna il software: una richiesta che funzionava smette di andare a buon fine. +Il motivo per cui prima funzionava e ora non più è quasi certamente un aggiornamento di sicurezza nel server di Neo4j (o nel server web sottostante, Jetty). - response = HTTP.withHeaders("Accept", "text/turtle").GET( - resolveURI(neo4j.httpURI(), "neo4j/describe/") + +Perché Funzionava Prima e Ora Non Più? +Nelle versioni precedenti, il server era più permissivo e consentiva la presenza di slash codificati (%2F) all'interno del percorso (path) di un URL. + +Tuttavia, permettere questa pratica è considerato un rischio per la sicurezza, perché può portare a vulnerabilità note come "Path Traversal Attacks". Per questo motivo, le versioni più recenti di quasi tutti i server web, per impostazione predefinita, rifiutano queste richieste con un errore 400 Bad Request. + +In sintesi: non è cambiata la logica dell'API, ma sono aumentate le misure di sicurezza del server che la ospita. + */ + + String fullEncoded = URLEncoder.encode("http://n4j.com/tst1/ontologies/2017/4/Cyber_EA_Smart_City#RF_signal_strength", StandardCharsets.UTF_8); + fullEncoded = fullEncoded.replace("%2F", "/"); + + String urlOld = + HTTP.GET(neo4j.httpURI().resolve("rdf").toString()).location() + "neo4j/describe?nodeIdentifier=" + URLEncoder.encode("http://n4j.com/tst1/ontologies/2017/4/Cyber_EA_Smart_City#RF_signal_strength", - "UTF-8")); + "UTF-8"); + + response = HTTP.withHeaders("Accept", "text/turtle").GET(neo4j.httpURI() + "rdf/neo4j/describe?nodeIdentifier=http%3A%2F%2Fn4j.com%2Ftst1%2Fontologies%2F2017%2F4%2FCyber_EA_Smart_City%23RF_signal_strength" +// resolveURI(neo4j.httpURI(), "neo4j/describe/ontologies") + +// resolveURI(neo4j.httpURI(), "neo4j/describe/") + "httpn4j.com/tst1/ontologies/2017/4/Cyber_EA_Smart_City#RF_signal_strength" +// resolveURI(neo4j.httpURI(), "neo4j/describe/") + URLEncoder.encode("t/4/Cyber_EA_Smart_City", StandardCharsets.UTF_8) +// URLEncoder.encode("http://n4j.com/tst1/ontologies/2017/4/Cyber_EA_Smart_City#RF_signal_strength", +// URLEncoder.encode("4/Cyber_EA_Smart_City#RF_signal_strength", +//// URLEncoder.encode("RF_signal_strength", +// "UTF-8") + ); + +// response = HTTP.withHeaders("Accept", "text/turtle").GET( +//// resolveURI(neo4j.httpURI(), "neo4j/describe/ontologies") + +// resolveURI(neo4j.httpURI(), "neo4j/describe/") + "httpn4j.com/tst1/ontologies/2017/4/Cyber_EA_Smart_City#RF_signal_strength" +//// resolveURI(neo4j.httpURI(), "neo4j/describe/") + URLEncoder.encode("t/4/Cyber_EA_Smart_City", StandardCharsets.UTF_8) +//// URLEncoder.encode("http://n4j.com/tst1/ontologies/2017/4/Cyber_EA_Smart_City#RF_signal_strength", +//// URLEncoder.encode("4/Cyber_EA_Smart_City#RF_signal_strength", +////// URLEncoder.encode("RF_signal_strength", +//// "UTF-8") +// ); assertEquals(200, response.status()); assertTrue(ModelTestUtils @@ -936,6 +985,7 @@ public void testFindNodeByLabelAndProperty() throws Exception { // When HTTP.Response response = HTTP.withHeaders("Accept", "application/ld+json").GET( resolveURI(neo4j.httpURI(), "neo4j/describe/find/Director/born/1961?valType=INTEGER")); +// resolveURI(neo4j.httpURI(), "neo4j/describe?nodeIdentifier=find/Director/born/1961?valType=INTEGER")); String expected = String.format("[ {\n" + " \"@id\" : \"neo4j://graph.individuals#%s\",\n" @@ -1025,29 +1075,28 @@ public void testFindNodeByLabelAndPropertyNotFoundOrInvalid() throws Exception { HTTP.Response response = HTTP.withHeaders("Accept", "application/ld+json").GET( resolveURI(neo4j.httpURI(), "neo4j/describe/find/WrongLabel/wrongProperty/someValue")); + // TODO - document it?? assertEquals(emptyJsonLd, response.rawContent()); assertEquals(200, response.status()); response = HTTP.withHeaders("Accept", "application/ld+json").GET( resolveURI(neo4j.httpURI(), "neo4j/describe/find/Something")); - assertEquals("", response.rawContent()); + assertEquals("{\"errors\":[{\"code\":\"Neo.ClientError.Request.Invalid\",\"message\":\"Not Found\"}]}", response.rawContent()); assertEquals(404, response.status()); - - } @Test public void testGetNodeByUriOrIdNotFoundOrInvalid() throws Exception { HTTP.Response response = HTTP.withHeaders("Accept", "text/n3").GET( - resolveURI(neo4j.httpURI(), "neo4j/describe/9999999")); + resolveURI(neo4j.httpURI(), "neo4j/describe?nodeIdentifier=9999999")); assertEquals(200, response.status()); assertEquals("@prefix n4sch: .\n" + "@prefix n4ind: .\n", response.rawContent()); response = HTTP.withHeaders("Accept", "application/rdf+xml").GET( - resolveURI(neo4j.httpURI(), "neo4j/describe/9999999")); + resolveURI(neo4j.httpURI(), "neo4j/describe?nodeIdentifier=9999999")); assertEquals(200, response.status()); assertEquals("\n" + "", response.rawContent()); response = HTTP.withHeaders("Accept", "application/ld+json").GET( - resolveURI(neo4j.httpURI(), "neo4j/describe/adb")); + resolveURI(neo4j.httpURI(), "neo4j/describe?nodeIdentifier=adb")); assertEquals("[ ]", response.rawContent()); assertEquals(200, response.status()); @@ -1069,12 +1118,12 @@ public void testGetNodeByUriOrIdNotFoundOrInvalid() throws Exception { } response = HTTP.withHeaders("Accept", "text/n3").GET( - resolveURI(neo4j.httpURI(), "neo4j/describe/9999999")); + resolveURI(neo4j.httpURI(), "neo4j/describe?nodeIdentifier=9999999")); assertEquals(200, response.status()); assertEquals("", response.rawContent()); response = HTTP.withHeaders("Accept", "application/rdf+xml").GET( - resolveURI(neo4j.httpURI(), "neo4j/describe/9999999")); + resolveURI(neo4j.httpURI(), "neo4j/describe?nodeIdentifier=9999999")); assertEquals(200, response.status()); assertEquals("\n" + " a ;\n" @@ -1504,10 +1553,10 @@ public void testNodeByUriAfterImport() throws Exception { } HTTP.Response response = HTTP.withHeaders("Accept", "application/rdf+xml").GET( - resolveURI(neo4j.httpURI(), "neo4j/describe/") + URLEncoder.encode( + resolveURI(neo4j.httpURI(), "neo4j/describe?nodeIdentifier=") + URLEncoder.encode( "https://spec.edmcouncil.org/fibo/ontology/BE/Corporations/Corporations/BoardAgreement", StandardCharsets.UTF_8.toString()) - + "?excludeContext=true"); + + "&excludeContext=true"); String expected = "" + " \n" @@ -2500,10 +2549,10 @@ public void testNodeByUriWithGraphUriOnQuadRDFNQuads() throws Exception { } HTTP.Response response = HTTP.withHeaders("Accept", "application/n-quads").GET( - resolveURI(neo4j.httpURI(), "neo4j/describe/") + URLEncoder + resolveURI(neo4j.httpURI(), "neo4j/describe?nodeIdentifier=") + URLEncoder .encode("http://www.example.org/exampleDocument#Monica", StandardCharsets.UTF_8.toString()) - + "?graphuri=http://www.example.org/exampleDocument%23G1"); + + "&graphuri=http://www.example.org/exampleDocument%23G1"); String expected = " \"Monica Murphy\" .\n" + " .\n" From be313b573ee646b8f64c6abb2c953a40bac53141 Mon Sep 17 00:00:00 2001 From: vga91 Date: Tue, 29 Jul 2025 09:39:10 +0200 Subject: [PATCH 2/2] small changes --- docs/antora.yml | 6 +- .../ROOT/pages/appendix_migration2025.adoc | 6 +- docs/modules/ROOT/pages/reference.adoc | 2 +- pom.xml | 27 ++++----- src/main/java/n10s/CommonProcedures.java | 1 - src/main/java/n10s/endpoint/RDFEndpoint.java | 23 +------- .../java/n10s/endpoint/RDFEndpointTest.java | 59 ++----------------- 7 files changed, 27 insertions(+), 97 deletions(-) diff --git a/docs/antora.yml b/docs/antora.yml index 0b07859c..3d11cbb5 100644 --- a/docs/antora.yml +++ b/docs/antora.yml @@ -1,13 +1,13 @@ name: neosemantics -version: '5.20' +version: '2025.06' title: Neosemantics nav: - modules/ROOT/nav.adoc asciidoc: attributes: - docs-version: 5.20 - page-neo4jversion: 5.20 + docs-version: 2025.06 + page-neo4jversion: 2025.06 page-product: Neosemantics page-type: Neosemantics Manual page-canonical-root: /labs/ diff --git a/docs/modules/ROOT/pages/appendix_migration2025.adoc b/docs/modules/ROOT/pages/appendix_migration2025.adoc index 593bd2de..7ae414ab 100644 --- a/docs/modules/ROOT/pages/appendix_migration2025.adoc +++ b/docs/modules/ROOT/pages/appendix_migration2025.adoc @@ -14,7 +14,9 @@ This documentation is intended for users who are familiar with neosemantics. Bas ==== Changes to the URL structure of the RDF endpoint -Starting with Neo4j versions that use recent versions of the Jetty web server (like Jetty 12 in Neo4j 2025.x), the structure for URLs sent to the RDF description endpoint has become stricter. Passing a full URI containing encoded slashes (`%2F`) directly within the URL path is no longer permitted and will result in an `HTTP 400 Bad Request` error. +Starting with Neo4j 2025.x that use recent versions of the Jetty web server (that is Jetty 12), the structure for URLs sent to the RDF description endpoint has become stricter. Passing a full URI containing encoded slashes (`%2F`) directly within the URL path is no longer permitted and will result in an `HTTP 400 Bad Request` error. + +Therefore, the `/rdf//describe/` endpoint is changed. This change is a direct consequence of enhanced security policies in Jetty. Specifically, the `UriCompliance` mode, which is stricter by default, now flags ambiguous path separators to prevent potential 'Path Traversal' vulnerabilities. For more technical details, see the Jetty issue https://github.com/jetty/jetty.project/issues/12162[#12162] and the `AMBIGUOUS_PATH_SEPARATOR` definition in the https://github.com/jetty/jetty.project/blob/jetty-12.0.x/jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/UriCompliance.java#L75[UriCompliance source code]. @@ -25,7 +27,7 @@ Note that the URI should be encoded as before. |=== | Previous Method (No longer supported) | Correct Method (Required) | The full URI was part of the *path*. | The full URI is passed as the `nodeidentifier` *query parameter*. -| `.../describe/http%3A%2F%2Fneo4j.org%2Find%23neo4j355` | `.../describe?nodeidentifier=http%3A%2F%2Fneo4j.org%2Find%23neo4j355` +| `/rdf//describe/http%3A%2F%2Fneo4j.org%2Find%23neo4j355` | `/rdf//describe?nodeidentifier=http%3A%2F%2Fneo4j.org%2Find%23neo4j355` |=== .Request Transformation Example diff --git a/docs/modules/ROOT/pages/reference.adoc b/docs/modules/ROOT/pages/reference.adoc index dc7a59dc..c257827b 100644 --- a/docs/modules/ROOT/pages/reference.adoc +++ b/docs/modules/ROOT/pages/reference.adoc @@ -308,7 +308,7 @@ a| [cols="15,5,45,35"] |=== | method| type| params| Description -| /rdf//describe/ +| /rdf//describe |GET a| * the id of a node or the (urlencoded) uri diff --git a/pom.xml b/pom.xml index e8ab7e2e..1bf9c729 100644 --- a/pom.xml +++ b/pom.xml @@ -36,23 +36,22 @@ 2025.06.2 4.3.12 - 2.13.3 - + 2.19.0 UTF-8 - - - - - - - - - - - - + + + + + com.fasterxml.jackson + jackson-bom + ${jackson.version} + pom + import + + + diff --git a/src/main/java/n10s/CommonProcedures.java b/src/main/java/n10s/CommonProcedures.java index 8e8b5222..76c92d2d 100644 --- a/src/main/java/n10s/CommonProcedures.java +++ b/src/main/java/n10s/CommonProcedures.java @@ -232,7 +232,6 @@ public static boolean isRedirect(HttpURLConnection con) throws IOException { protected RDFFormat getFormat(String format) throws RDFImportBadParams { - System.out.println("availableParsers = " + Arrays.stream(availableParsers).map(FileFormat::getName).toList()); if (format != null) { for (RDFFormat parser : availableParsers) { if (parser.getName().equals(format)) { diff --git a/src/main/java/n10s/endpoint/RDFEndpoint.java b/src/main/java/n10s/endpoint/RDFEndpoint.java index 66a1ab21..7661c1d7 100644 --- a/src/main/java/n10s/endpoint/RDFEndpoint.java +++ b/src/main/java/n10s/endpoint/RDFEndpoint.java @@ -77,15 +77,8 @@ public Response ping() throws IOException { }}; return Response.ok().entity(objectMapper.writeValueAsString(results)).build(); } - - /* - HTTP error 400 (Bad Request) occurs because the slash (/) in your identifier, - once encoded in %2F by URLEncoder, - is often blocked for security reasons by the application server (such as Tomcat, WildFly) or by a proxy before it even reaches your JAX-RS code. - */ + @GET -// @Path("/{dbname}/describe/{nodeidentifier}") -// @Path("/{dbname}/describe/{nodeidentifier:.+}") @Path("/{dbname}/describe") @Produces({"application/rdf+xml", "text/plain", "text/turtle", "text/n3", "application/trig", "application/ld+json", "application/n-quads", "text/x-turtlestar", @@ -99,20 +92,6 @@ public Response nodebyIdOrUri(@Context DatabaseManagementService gds, @QueryParam("format") String format, @HeaderParam("accept") String acceptHeaderParam) { return Response.ok().entity((StreamingOutput) outputStream -> { -// @GET -// @Path("/{dbname}/describe/{nodeidentifier}") -// @Produces({"application/rdf+xml", "text/plain", "text/turtle", "text/n3", -// "application/trig", "application/ld+json", "application/n-quads", "text/x-turtlestar", -// "application/x-trigstar"}) -// public Response nodebyIdOrUri(@Context DatabaseManagementService gds, -// @PathParam("dbname") String dbNameParam, -// @PathParam("nodeidentifier") String nodeIdentifier, -// @QueryParam("graphuri") String namedGraphId, -// @QueryParam("excludeContext") String excludeContextParam, -// @QueryParam("mappedElemsOnly") String onlyMappedInfo, -// @QueryParam("format") String format, -// @HeaderParam("accept") String acceptHeaderParam) { -// return Response.ok().entity((StreamingOutput) outputStream -> { RDFWriter writer = startRdfWriter(getFormat(acceptHeaderParam, format), outputStream); GraphDatabaseService neo4j = gds.database(dbNameParam); try (Transaction tx = neo4j.beginTx()) { diff --git a/src/test/java/n10s/endpoint/RDFEndpointTest.java b/src/test/java/n10s/endpoint/RDFEndpointTest.java index 34658382..620aad27 100644 --- a/src/test/java/n10s/endpoint/RDFEndpointTest.java +++ b/src/test/java/n10s/endpoint/RDFEndpointTest.java @@ -438,7 +438,6 @@ public void testPrefixwithHyphen() throws Exception { } Response response = HTTP.withHeaders("Accept", "text/plain").GET( -// resolveURI(neo4j.httpURI(), "neo4j/describe?nodeIdentifier=http%3A%2F%2Fwww.example.com%2Fexample%23Enitity1Individual&format=RDF/XML")); resolveURI(neo4j.httpURI(), "neo4j/describe?nodeIdentifier=" + URLEncoder.encode("http://www.example.com/example#Enitity1Individual", StandardCharsets.UTF_8) + "&format=RDF/XML")); String expected = @@ -820,21 +819,7 @@ public void ImportGetNodeByUriOnImportedOntoIgnore() throws Exception { // then export elements and check the output is right HTTP.Response response = HTTP.withHeaders("Accept", "text/turtle").GET( resolveURI(neo4j.httpURI(), "neo4j/describe?nodeIdentifier=" + id)); -// -// -// -//Error 400 Ambiguous URI path separator -// -// -//

HTTP ERROR 400 Ambiguous URI path separator

-// -// -// -// -//
URI:/badURI
STATUS:400
MESSAGE:Ambiguous URI path separator
-// -// -// + String expected = "@prefix rdf: .\n" + "@prefix neovoc: .\n" + "@prefix neoind: .\n" + @@ -846,46 +831,12 @@ public void ImportGetNodeByUriOnImportedOntoIgnore() throws Exception { assertEquals(200, response.status()); assertTrue(ModelTestUtils .compareModels(expected, RDFFormat.TURTLE, response.rawContent(), RDFFormat.TURTLE)); - - /* TODO - Capisco perfettamente la tua frustrazione. È una situazione comune quando si aggiorna il software: una richiesta che funzionava smette di andare a buon fine. -Il motivo per cui prima funzionava e ora non più è quasi certamente un aggiornamento di sicurezza nel server di Neo4j (o nel server web sottostante, Jetty). - -Perché Funzionava Prima e Ora Non Più? -Nelle versioni precedenti, il server era più permissivo e consentiva la presenza di slash codificati (%2F) all'interno del percorso (path) di un URL. - -Tuttavia, permettere questa pratica è considerato un rischio per la sicurezza, perché può portare a vulnerabilità note come "Path Traversal Attacks". Per questo motivo, le versioni più recenti di quasi tutti i server web, per impostazione predefinita, rifiutano queste richieste con un errore 400 Bad Request. - -In sintesi: non è cambiata la logica dell'API, ma sono aumentate le misure di sicurezza del server che la ospita. - */ - - String fullEncoded = URLEncoder.encode("http://n4j.com/tst1/ontologies/2017/4/Cyber_EA_Smart_City#RF_signal_strength", StandardCharsets.UTF_8); - fullEncoded = fullEncoded.replace("%2F", "/"); - - String urlOld = - HTTP.GET(neo4j.httpURI().resolve("rdf").toString()).location() + "neo4j/describe?nodeIdentifier=" + + + response = HTTP.withHeaders("Accept", "text/turtle").GET( + resolveURI(neo4j.httpURI(), "neo4j/describe?nodeIdentifier=") + URLEncoder.encode("http://n4j.com/tst1/ontologies/2017/4/Cyber_EA_Smart_City#RF_signal_strength", - "UTF-8"); - - response = HTTP.withHeaders("Accept", "text/turtle").GET(neo4j.httpURI() + "rdf/neo4j/describe?nodeIdentifier=http%3A%2F%2Fn4j.com%2Ftst1%2Fontologies%2F2017%2F4%2FCyber_EA_Smart_City%23RF_signal_strength" -// resolveURI(neo4j.httpURI(), "neo4j/describe/ontologies") + -// resolveURI(neo4j.httpURI(), "neo4j/describe/") + "httpn4j.com/tst1/ontologies/2017/4/Cyber_EA_Smart_City#RF_signal_strength" -// resolveURI(neo4j.httpURI(), "neo4j/describe/") + URLEncoder.encode("t/4/Cyber_EA_Smart_City", StandardCharsets.UTF_8) -// URLEncoder.encode("http://n4j.com/tst1/ontologies/2017/4/Cyber_EA_Smart_City#RF_signal_strength", -// URLEncoder.encode("4/Cyber_EA_Smart_City#RF_signal_strength", -//// URLEncoder.encode("RF_signal_strength", -// "UTF-8") + StandardCharsets.UTF_8) ); - -// response = HTTP.withHeaders("Accept", "text/turtle").GET( -//// resolveURI(neo4j.httpURI(), "neo4j/describe/ontologies") + -// resolveURI(neo4j.httpURI(), "neo4j/describe/") + "httpn4j.com/tst1/ontologies/2017/4/Cyber_EA_Smart_City#RF_signal_strength" -//// resolveURI(neo4j.httpURI(), "neo4j/describe/") + URLEncoder.encode("t/4/Cyber_EA_Smart_City", StandardCharsets.UTF_8) -//// URLEncoder.encode("http://n4j.com/tst1/ontologies/2017/4/Cyber_EA_Smart_City#RF_signal_strength", -//// URLEncoder.encode("4/Cyber_EA_Smart_City#RF_signal_strength", -////// URLEncoder.encode("RF_signal_strength", -//// "UTF-8") -// ); assertEquals(200, response.status()); assertTrue(ModelTestUtils