diff --git a/gremlin-dotnet/docker-compose.yml b/gremlin-dotnet/docker-compose.yml index 244bd3f289e..36d2f55f35d 100644 --- a/gremlin-dotnet/docker-compose.yml +++ b/gremlin-dotnet/docker-compose.yml @@ -50,6 +50,7 @@ services: - SOCKET_SERVER_VERSION=${GREMLIN_SERVER} ports: - "45943:45943" + - "45944:45944" gremlin-dotnet-integration-tests: container_name: gremlin-dotnet-integration-tests @@ -65,6 +66,7 @@ services: - GREMLIN_SERVER_PORT=45940 - GREMLIN_SECURE_SERVER_PORT=45941 - GREMLIN_SOCKET_SERVER_URL=http://gremlin-socket-server-test-dotnet:45943/gremlin + - GREMLIN_SOCKET_SERVER_PROXY_URL=http://gremlin-socket-server-test-dotnet:45944 - VERTEX_LABEL=dotnet-example working_dir: /gremlin-dotnet command: > diff --git a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Driver/ProxyBehaviorIntegrationTests.cs b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Driver/ProxyBehaviorIntegrationTests.cs new file mode 100644 index 00000000000..5653cabbafe --- /dev/null +++ b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Driver/ProxyBehaviorIntegrationTests.cs @@ -0,0 +1,108 @@ +#region License + +/* + * 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. + */ + +#endregion + +using System; +using System.Net; +using System.Net.Http; +using System.Text.Json; +using System.Threading.Tasks; +using Gremlin.Net.Driver; +using Xunit; + +namespace Gremlin.Net.IntegrationTest.Driver +{ + public class ProxyBehaviorIntegrationTests + { + private static readonly string Host = GetHost(); + private static readonly string ProxyUrl = GetProxyUrl(); + + private static string GetHost() + { + var url = Environment.GetEnvironmentVariable("GREMLIN_SOCKET_SERVER_URL"); + if (string.IsNullOrEmpty(url)) return "localhost"; + try + { + return new Uri(url).Host; + } + catch + { + return "localhost"; + } + } + + private static string GetProxyUrl() + { + var url = Environment.GetEnvironmentVariable("GREMLIN_SOCKET_SERVER_PROXY_URL"); + return string.IsNullOrEmpty(url) ? "http://localhost:45944" : url; + } + + private static async Task ResetProxyAsync(HttpClient httpClient) + { + using var response = await httpClient.PostAsync($"{ProxyUrl}/__reset", null); + response.EnsureSuccessStatusCode(); + } + + private static async Task GetRecordedAsync(HttpClient httpClient) + { + var json = await httpClient.GetStringAsync($"{ProxyUrl}/__recorded"); + return JsonSerializer.Deserialize(json) ?? Array.Empty(); + } + + [Fact] + public async Task ShouldRouteSocketServerTrafficThroughProxy() + { + using var httpClient = new HttpClient(); + await ResetProxyAsync(httpClient); + + var server = new GremlinServer(Host, SocketServerConstants.Port); + using (var client = new GremlinClient(server, + connectionSettings: new ConnectionSettings { Proxy = new WebProxy(ProxyUrl) })) + { + var resultSet = await client.SubmitAsync(SocketServerConstants.GremlinSingleVertex); + var results = await resultSet.ToListAsync(); + Assert.Single(results); + } + + var recorded = await GetRecordedAsync(httpClient); + Assert.Contains(recorded, t => t.EndsWith(":45943")); + } + + [Fact] + public async Task ShouldNotRecordWhenNoProxy() + { + using var httpClient = new HttpClient(); + await ResetProxyAsync(httpClient); + + var server = new GremlinServer(Host, SocketServerConstants.Port); + using (var client = new GremlinClient(server)) + { + var resultSet = await client.SubmitAsync(SocketServerConstants.GremlinSingleVertex); + var results = await resultSet.ToListAsync(); + Assert.Single(results); + } + + var recorded = await GetRecordedAsync(httpClient); + Assert.DoesNotContain(recorded, t => t.EndsWith(":45943")); + } + } +} diff --git a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs index 7120c36fb8a..facc134fda9 100644 --- a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs +++ b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs @@ -2749,6 +2749,37 @@ private static IDictionary, ITraversal>> {(g,p) =>g.V().Has(T.Label, "person")}}, {"g_V_hasXlabel_eqXpersonXX", new List, ITraversal>> {(g,p) =>g.V().Has(T.Label, P.Eq("person"))}}, {"g_V_hasXname_nullX", new List, ITraversal>> {(g,p) =>g.V().Has("name", (object) null)}}, + {"g_V_hasXname_constantXmarkoXX", new List, ITraversal>> {(g,p) =>g.V().Has("name", __.Constant("marko"))}}, + {"g_V_hasXname_VXvid1X_outXknowsX_valuesXnameXX", new List, ITraversal>> {(g,p) =>g.V().Has("name", __.V(p["vid1"]).Out("knows").Values("name").Order())}}, + {"g_V_hasXname_valuesXnonexistentXX", new List, ITraversal>> {(g,p) =>g.V().Has("name", __.Values("nonexistent"))}}, + {"g_V_hasXname_notXidentityXX", new List, ITraversal>> {(g,p) =>g.V().Has("name", __.Not(__.Identity()))}}, + {"g_V_hasXage_gtXconstantX29XXX", new List, ITraversal>> {(g,p) =>g.V().Has("age", P.Gt(__.Constant(29)))}}, + {"g_V_hasXage_lteXconstantX27XXX", new List, ITraversal>> {(g,p) =>g.V().Has("age", P.Lte(__.Constant(27)))}}, + {"g_V_hasXage_neqXconstantX32XXX", new List, ITraversal>> {(g,p) =>g.V().Has("age", P.Neq(__.Constant(32)))}}, + {"g_V_hasXname_eqXconstantXmarkoXXX", new List, ITraversal>> {(g,p) =>g.V().Has("name", P.Eq(__.Constant("marko")))}}, + {"g_V_hasXage_ltXconstantX29XXX", new List, ITraversal>> {(g,p) =>g.V().Has("age", P.Lt(__.Constant(29)))}}, + {"g_V_hasXage_gteXconstantX32XXX", new List, ITraversal>> {(g,p) =>g.V().Has("age", P.Gte(__.Constant(32)))}}, + {"g_V_hasXage_eqXvaluesXnonexistentXXX", new List, ITraversal>> {(g,p) =>g.V().Has("age", P.Eq(__.Values("nonexistent")))}}, + {"g_V_hasXlabel_constantXpersonXX", new List, ITraversal>> {(g,p) =>g.V().Has(T.Label, __.Constant("person"))}}, + {"g_V_hasXperson_name_constantXmarkoXX_age", new List, ITraversal>> {(g,p) =>g.V().Has("person", "name", __.Constant("marko")).Values("age")}}, + {"g_V_hasXname_withinXunionXVXvid1X_outXknowsX_valuesXnameX_constantXpeterXX_foldXX", new List, ITraversal>> {(g,p) =>g.V().Has("name", P.Within(__.Union(__.V(p["vid1"]).Out("knows").Values("name"), __.Constant("peter")).Fold()))}}, + {"g_V_hasXname_withinXvaluesXnonexistentX_constantXmarkoXXX", new List, ITraversal>> {(g,p) =>g.V().Has("name", P.Within(__.Values("nonexistent"), __.Constant("marko")))}}, + {"g_V_hasXname_withinXvaluesXnonexistentX_valuesXnonexistent2XXX", new List, ITraversal>> {(g,p) =>g.V().Has("name", P.Within(__.Values("nonexistent"), __.Values("nonexistent2")))}}, + {"g_V_hasXname_withoutXconstantXmarkoX_constantXvadasX_constantXpeterXXX", new List, ITraversal>> {(g,p) =>g.V().Has("name", P.Without(__.Constant("marko"), __.Constant("vadas"), __.Constant("peter")))}}, + {"g_V_hasXname_withinXunionXVXvid1X_outXknowsX_valuesXnameX_VXvid3X_outXcreatedX_valuesXnameXX_foldXX", new List, ITraversal>> {(g,p) =>g.V().Has("name", P.Within(__.Union(__.V(p["vid1"]).Out("knows").Values("name"), __.V(p["vid3"]).Out("created").Values("name")).Fold()))}}, + {"g_V_hasLabelXsoftwareX_hasXname_withoutXunionXVXvid1X_outXcreatedX_valuesXnameX_VXvid4X_outXcreatedX_valuesXnameXX_foldXX", new List, ITraversal>> {(g,p) =>g.V().HasLabel("software").Has("name", P.Without(__.Union(__.V(p["vid1"]).Out("created").Values("name"), __.V(p["vid4"]).Out("created").Values("name")).Fold()))}}, + {"g_V_hasLabelXpersonX_valuesXageX_isXwithinXunionXVXvid1X_valuesXageX_V_hasXname_lopX_inXcreatedX_valuesXageXX_foldXX", new List, ITraversal>> {(g,p) =>g.V().HasLabel("person").Values("age").Is(P.Within(__.Union(__.V(p["vid1"]).Values("age"), __.V().Has("name", "lop").In("created").Values("age")).Fold()))}}, + {"g_VXvid1X_outEXknowsX_filterXinV_hasXname_withinXunionXV_hasXname_lopX_inXcreatedX_valuesXnameX_V_hasXname_rippleX_inXcreatedX_valuesXnameXX_foldXXXX", new List, ITraversal>> {(g,p) =>g.V(p["vid1"]).OutE("knows").Filter(__.InV().Has("name", P.Within(__.Union(__.V().Has("name", "lop").In("created").Values("name"), __.V().Has("name", "ripple").In("created").Values("name")).Fold())))}}, + {"g_V_hasLabelXconstantXpersonXX", new List, ITraversal>> {(g,p) =>g.V().HasLabel(__.Constant("person"))}}, + {"g_V_hasIdXeqXVXvid1X_idXX", new List, ITraversal>> {(g,p) =>g.V().HasId(P.Eq(__.V(p["vid1"]).Id()))}}, + {"g_V_hasIdXVXvid1X_idXX", new List, ITraversal>> {(g,p) =>g.V().HasId(__.V(p["vid1"]).Id())}}, + {"g_V_propertiesXageX_hasKeyXconstantXageXX", new List, ITraversal>> {(g,p) =>g.V().Properties("age").HasKey(__.Constant("age"))}}, + {"g_V_propertiesXageX_hasKeyXeqXconstantXageXXX", new List, ITraversal>> {(g,p) =>g.V().Properties("age").HasKey(P.Eq(__.Constant("age")))}}, + {"g_V_propertiesXageX_hasValueXconstantX29XX", new List, ITraversal>> {(g,p) =>g.V().Properties("age").HasValue(__.Constant(29))}}, + {"g_V_propertiesXageX_hasValueXeqXconstantX29XXX", new List, ITraversal>> {(g,p) =>g.V().Properties("age").HasValue(P.Eq(__.Constant(29)))}}, + {"g_VXvid1X_asXaX_V_hasXage_gtXselectXaX_valuesXageXXX_valuesXnameX", new List, ITraversal>> {(g,p) =>g.V(p["vid1"]).As("a").V().Has("age", P.Gt(__.Select("a").Values("age"))).Values("name")}}, + {"g_withSackX29X_V_hasXage_gtXsackXX_valuesXnameX", new List, ITraversal>> {(g,p) =>g.WithSack(29).V().Has("age", P.Gt(__.Sack())).Values("name")}}, + {"g_V_hasXname_markoX_outEXknowsX_hasXweight_gteXconstantX05XXX", new List, ITraversal>> {(g,p) =>g.V().Has("name", "marko").OutE("knows").Has("weight", P.Gte(__.Constant(0.5)))}}, {"g_V_hasIdXemptyX_count", new List, ITraversal>> {(g,p) =>g.V().HasId(p["xx1"]).Count()}}, {"g_V_hasIdXwithinXemptyXX_count", new List, ITraversal>> {(g,p) =>g.V().HasId(P.Within(new List { })).Count()}}, {"g_V_hasIdXwithoutXemptyXX_count", new List, ITraversal>> {(g,p) =>g.V().HasId(P.Without(new List { })).Count()}}, @@ -2808,6 +2839,28 @@ private static IDictionary, ITraversal>> {(g,p) =>g.V().Values("age").Is(P.Gte(p["xx1"])).Is(P.Lt(p["xx2"]))}}, {"g_V_whereXinXcreatedX_count_isX1XX_valuesXnameX", new List, ITraversal>> {(g,p) =>g.V().Where(__.In("created").Count().Is(1)).Values("name")}}, {"g_V_whereXinXcreatedX_count_isXgte_2XX_valuesXnameX", new List, ITraversal>> {(g,p) =>g.V().Where(__.In("created").Count().Is(P.Gte(2))).Values("name")}}, + {"g_V_valuesXageX_isXconstantX29XX", new List, ITraversal>> {(g,p) =>g.V().Values("age").Is(__.Constant(29))}}, + {"g_V_valuesXnameX_isXconstantXmarkoXX", new List, ITraversal>> {(g,p) =>g.V().Values("name").Is(__.Constant("marko"))}}, + {"g_V_valuesXageX_isXgtXconstantX29XXX", new List, ITraversal>> {(g,p) =>g.V().Values("age").Is(P.Gt(__.Constant(29)))}}, + {"g_V_valuesXageX_isXltXconstantX32XXX", new List, ITraversal>> {(g,p) =>g.V().Values("age").Is(P.Lt(__.Constant(32)))}}, + {"g_V_valuesXageX_isXneqXconstantX35XXX", new List, ITraversal>> {(g,p) =>g.V().Values("age").Is(P.Neq(__.Constant(35)))}}, + {"g_V_valuesXageX_isXwithinXVXvid1X_outXknowsX_valuesXageXXX", new List, ITraversal>> {(g,p) =>g.V().Values("age").Is(P.Within(__.V(p["vid1"]).Out("knows").Values("age").Fold()))}}, + {"g_V_valuesXageX_isXconstantX99999XX", new List, ITraversal>> {(g,p) =>g.V().Values("age").Is(__.Constant(99999))}}, + {"g_V_hasLabelXpersonX_valuesXageX_chooseXgtXconstantX29XX_olderX", new List, ITraversal>> {(g,p) =>g.V().HasLabel("person").Values("age").Choose(__.Is(P.Gt(__.Constant(29))), __.Constant("older than marko"), __.Constant("not older"))}}, + {"g_V_hasLabelXpersonX_valuesXageX_chooseXgteXmeanAgeX_aboveX", new List, ITraversal>> {(g,p) =>g.V().HasLabel("person").Values("age").Choose(__.Is(P.Gte(__.V().HasLabel("person").Values("age").Mean())), __.Constant("above average"), __.Constant("below average"))}}, + {"g_VXvid1X_outXknowsX_valuesXageX_fold_allXgteXconstantX27XXX", new List, ITraversal>> {(g,p) =>g.V(p["vid1"]).Out("knows").Values("age").Fold().All(P.Gte(__.Constant(27)))}}, + {"g_VXvid1X_outXknowsX_valuesXageX_fold_allXgtXconstantX27XXX", new List, ITraversal>> {(g,p) =>g.V(p["vid1"]).Out("knows").Values("age").Fold().All(P.Gt(__.Constant(27)))}}, + {"g_VXvid1X_outXknowsX_valuesXageX_fold_anyXeqXconstantX32XXX", new List, ITraversal>> {(g,p) =>g.V(p["vid1"]).Out("knows").Values("age").Fold().Any(P.Eq(__.Constant(32)))}}, + {"g_VXvid1X_outXknowsX_valuesXageX_fold_noneXeqXconstantX35XXX", new List, ITraversal>> {(g,p) =>g.V(p["vid1"]).Out("knows").Values("age").Fold().None(P.Eq(__.Constant(35)))}}, + {"g_VXvid1X_outXknowsX_valuesXageX_fold_noneXeqXconstantX32XXX", new List, ITraversal>> {(g,p) =>g.V(p["vid1"]).Out("knows").Values("age").Fold().None(P.Eq(__.Constant(32)))}}, + {"g_injectXnullX_isXeqXV9999_valuesXnameXXX", new List, ITraversal>> {(g,p) =>g.Inject(null).Is(P.Eq(__.V(9999).Values("name")))}}, + {"g_injectXmarkoX_isXV9999_valuesXnameXX", new List, ITraversal>> {(g,p) =>g.Inject("marko").Is(__.V(9999).Values("name"))}}, + {"g_injectXmarkoX_chooseXeqXV9999_valuesXnameXX_matched_unmatchedX", new List, ITraversal>> {(g,p) =>g.Inject("marko").Choose(__.Is(P.Eq(__.V(9999).Values("name"))), __.Constant("matched"), __.Constant("unmatched"))}}, + {"g_injectXlistX_noneXeqXV9999_valuesXnameXXX", new List, ITraversal>> {(g,p) =>g.Inject(new List { "marko", "josh" }).None(P.Eq(__.V(9999).Values("name")))}}, + {"g_V_hasXname_eqXV9999_valuesXnameXXX", new List, ITraversal>> {(g,p) =>g.V().Has("name", P.Eq(__.V(9999).Values("name")))}}, + {"g_V_valuesXageX_isXwithoutXconstantX29X_constantX32XXX", new List, ITraversal>> {(g,p) =>g.V().Values("age").Is(P.Without(__.Constant(29), __.Constant(32)))}}, + {"g_VXvid1X_asXaX_V_valuesXageX_isXgtXselectXaX_valuesXageXXX", new List, ITraversal>> {(g,p) =>g.V(p["vid1"]).As("a").V().Values("age").Is(P.Gt(__.Select("a").Values("age")))}}, + {"g_withSackX30X_V_valuesXageX_isXgtXsackXX", new List, ITraversal>> {(g,p) =>g.WithSack(30).V().Values("age").Is(P.Gt(__.Sack()))}}, {"g_V_valuesXageX_noneXgtX32XX", new List, ITraversal>> {(g,p) =>g.V().Values("age").None(P.Gt(32))}}, {"g_V_valuesXageX_whereXisXP_gtX33XXX_fold_noneXlteX33XX", new List, ITraversal>> {(g,p) =>g.V().Values("age").Where(__.Is(P.Gt(33))).Fold().None(P.Lte(33))}}, {"g_V_valuesXageX_order_byXdescX_fold_noneXltX10XX", new List, ITraversal>> {(g,p) =>g.V().Values("age").Order().By(Order.Desc).Fold().None(P.Lt(10))}}, @@ -2980,6 +3033,16 @@ private static IDictionary, ITraversal>> {(g,p) =>g.V().Where(__.Values("age").Is(P.Gt(30)))}}, {"g_V_whereXlabel_isXsoftwareXX", new List, ITraversal>> {(g,p) =>g.V().Where(__.Label().Is("software"))}}, {"g_V_whereXlabel_isXpersonXX", new List, ITraversal>> {(g,p) =>g.V().Where(__.Label().Is("person"))}}, + {"g_V_valuesXageX_whereXgtXconstantX29XXX", new List, ITraversal>> {(g,p) =>g.V().Values("age").Where(P.Gt(__.Constant(29)))}}, + {"g_V_valuesXageX_whereXltXconstantX32XXX", new List, ITraversal>> {(g,p) =>g.V().Values("age").Where(P.Lt(__.Constant(32)))}}, + {"g_V_valuesXageX_whereXeqXconstantX29XXX", new List, ITraversal>> {(g,p) =>g.V().Values("age").Where(P.Eq(__.Constant(29)))}}, + {"g_V_valuesXageX_whereXwithinXVXvid1X_outXknowsX_valuesXageXXX", new List, ITraversal>> {(g,p) =>g.V().Values("age").Where(P.Within(__.V(p["vid1"]).Out("knows").Values("age").Fold()))}}, + {"g_V_valuesXnameX_whereXneqXconstantXmarkoXXX", new List, ITraversal>> {(g,p) =>g.V().Values("name").Where(P.Neq(__.Constant("marko")))}}, + {"g_V_valuesXageX_whereXeqXconstantX99999XXX", new List, ITraversal>> {(g,p) =>g.V().Values("age").Where(P.Eq(__.Constant(99999)))}}, + {"g_V_whereXgtXVXvid1X_valuesXageXXX_byXageX_valuesXnameX", new List, ITraversal>> {(g,p) =>g.V().Where(P.Gt(__.V(p["vid1"]).Values("age"))).By("age").Values("name")}}, + {"g_VX1X_asXaX_V_valuesXageX_whereXgteXconstantX0XX_andXneqXaXXX_byX_byXageX", new List, ITraversal>> {(g,p) =>g.V(p["vid1"]).As("a").V().HasLabel("person").Values("age").Where(P.Gte(__.Constant(0)).And(P.Neq("a"))).By().By("age")}}, + {"g_VX1X_asXaX_V_valuesXageX_whereXneqXaX_andXgtXVXvid1X_valuesXageXXXX_byX_byXageX", new List, ITraversal>> {(g,p) =>g.V(p["vid1"]).As("a").V().HasLabel("person").Values("age").Where(P.Neq("a").And(P.Gt(__.V(p["vid1"]).Values("age")))).By().By("age")}}, + {"g_VX1X_asXaX_V_valuesXageX_whereXeqXVXvid1X_valuesXageXX_orXeqXaXXX_byX_byXageX", new List, ITraversal>> {(g,p) =>g.V(p["vid1"]).As("a").V().HasLabel("person").Values("age").Where(P.Eq(__.V(p["vid1"]).Values("age")).Or(P.Eq("a"))).By().By("age")}}, {"g_withStrategiesXAdjacentToIncidentStrategyX_V", new List, ITraversal>> {(g,p) =>g.WithStrategies(new AdjacentToIncidentStrategy()).V()}}, {"g_withoutStrategiesXAdjacentToIncidentStrategyX_V", new List, ITraversal>> {(g,p) =>g.WithoutStrategies(typeof(AdjacentToIncidentStrategy)).V()}}, {"g_withStrategiesXAdjacentToIncidentStrategyX_V_out_count", new List, ITraversal>> {(g,p) =>g.WithStrategies(new AdjacentToIncidentStrategy()).V().Out().Count()}}, @@ -3101,6 +3164,23 @@ private static IDictionary, ITraversal>> {(g,p) =>g.WithoutStrategies(typeof(SeedStrategy)).V()}}, {"g_withStrategiesXStandardVerificationStrategyX_V", new List, ITraversal>> {(g,p) =>g.WithStrategies(new StandardVerificationStrategy()).V()}}, {"g_withoutStrategiesXStandardVerificationStrategyX_V", new List, ITraversal>> {(g,p) =>g.WithoutStrategies(typeof(StandardVerificationStrategy)).V()}}, + {"g_V_hasXname_addVXxX_valuesXnameXX_rejected", new List, ITraversal>> {(g,p) =>g.V().Has("name", __.AddV((string) "x").Values("name"))}}, + {"g_V_hasXname_V_drop_constantXxXX_rejected", new List, ITraversal>> {(g,p) =>g.V().Has("name", __.V().Drop().Constant("x"))}}, + {"g_V_hasXname_V_mapXaddVXxXX_valuesXnameXX_rejected", new List, ITraversal>> {(g,p) =>g.V().Has("name", __.V().Map(__.AddV((string) "x")).Values("name"))}}, + {"g_V_hasXname_eqXaddVXxX_valuesXnameXXX_rejected", new List, ITraversal>> {(g,p) =>g.V().Has("name", P.Eq(__.AddV((string) "x").Values("name")))}}, + {"g_V_hasXname_withinXaddVXxX_valuesXnameXXX_rejected", new List, ITraversal>> {(g,p) =>g.V().Has("name", P.Within(__.AddV((string) "x").Values("name")))}}, + {"g_V_hasXage_gtXaddVXxX_valuesXageXXX_rejected", new List, ITraversal>> {(g,p) =>g.V().Has("age", P.Gt(__.AddV((string) "x").Values("age")))}}, + {"g_V_valuesXageX_isXaddVXxX_valuesXageXX_rejected", new List, ITraversal>> {(g,p) =>g.V().Values("age").Is(__.AddV((string) "x").Values("age"))}}, + {"g_V_valuesXageX_isXgtXaddVXxX_valuesXageXXX_rejected", new List, ITraversal>> {(g,p) =>g.V().Values("age").Is(P.Gt(__.AddV((string) "x").Values("age")))}}, + {"g_V_VXaddVXxX_idX_rejected", new List, ITraversal>> {(g,p) =>g.V().V(__.AddV((string) "x").Id())}}, + {"g_V_EXaddVXxX_idX_rejected", new List, ITraversal>> {(g,p) =>g.V().E(__.AddV((string) "x").Id())}}, + {"g_VXaddVXxX_idX_rejected", new List, ITraversal>> {(g,p) =>g.V(__.AddV((string) "x").Id())}}, + {"g_EXaddVXxX_idX_rejected", new List, ITraversal>> {(g,p) =>g.E(__.AddV((string) "x").Id())}}, + {"g_V_propertyXV_mapXdropX_projectXxX_byXnameXX_rejected", new List, ITraversal>> {(g,p) =>g.V().Property((ITraversal) __.V().Map(__.Drop()).Project("x").By("name"))}}, + {"g_V_propertyXaddVXtempX_projectXkX_byXnameXX_rejected", new List, ITraversal>> {(g,p) =>g.V().Property((ITraversal) __.AddV((string) "temp").Project("k").By("name"))}}, + {"g_V_hasXname_VXvid1X_valuesXnameXX_passes_verification", new List, ITraversal>> {(g,p) =>g.V().Has("name", __.V(p["vid1"]).Values("name"))}}, + {"g_V_hasXage_gtXVXvid1X_valuesXageXXX_passes_verification", new List, ITraversal>> {(g,p) =>g.V().Has("age", P.Gt(__.V(p["vid1"]).Values("age")))}}, + {"g_V_VXoutXknowsX_idX_valuesXnameX_passes_verification", new List, ITraversal>> {(g,p) =>g.V(p["vid1"]).V(__.Out("knows").Id()).Values("name")}}, {"g_withStrategiesXSubgraphStrategyXsubgraphAXX_V", new List, ITraversal>> {(g,p) =>g.WithStrategies(new SubgraphStrategy(vertices: __.Has("name", P.Within("josh", "lop", "ripple")))).V()}}, {"g_withStrategiesXSubgraphStrategyXsubgraphAXX_E", new List, ITraversal>> {(g,p) =>g.WithStrategies(new SubgraphStrategy(vertices: __.Has("name", P.Within("josh", "lop", "ripple")))).E()}}, {"g_withStrategiesXSubgraphStrategyXsubgraphAXX_VX4X_outE", new List, ITraversal>> {(g,p) =>g.WithStrategies(new SubgraphStrategy(vertices: __.Has("name", P.Within("josh", "lop", "ripple")))).V(p["vid4"]).OutE()}}, @@ -3211,9 +3291,9 @@ private static IDictionary, ITraversal>> {(g,p) =>g.AddV().Property(Cardinality.Single, new Dictionary {{ "name", "foo" }, { "age", 42 }}), (g,p) =>g.V().Has("name", "foo")}}, {"g_V_hasXname_fooX_propertyXname_setXbarX_age_43X", new List, ITraversal>> {(g,p) =>g.AddV().Property(Cardinality.Single, "name", "foo").Property("age", 42), (g,p) =>g.V().Has("name", "foo").Property(new Dictionary {{ "name", CardinalityValue.Set("bar") }, { "age", 43 }}), (g,p) =>g.V().Has("name", "foo"), (g,p) =>g.V().Has("name", "bar"), (g,p) =>g.V().Has("age", 43), (g,p) =>g.V().Has("age", 42)}}, {"g_V_hasXname_fooX_propertyXset_name_bar_age_singleX43XX", new List, ITraversal>> {(g,p) =>g.AddV().Property(Cardinality.Single, "name", "foo").Property("age", 42), (g,p) =>g.V().Has("name", "foo").Property(Cardinality.Set, new Dictionary {{ "name", "bar" }, { "age", CardinalityValue.Single(43) }}), (g,p) =>g.V().Has("name", "foo"), (g,p) =>g.V().Has("name", "bar"), (g,p) =>g.V().Has("age", 43), (g,p) =>g.V().Has("age", 42)}}, - {"g_addV_propertyXnullX", new List, ITraversal>> {(g,p) =>g.AddV((string) "person").Property(null), (g,p) =>g.V().HasLabel("person").Values()}}, + {"g_addV_propertyXnullX", new List, ITraversal>> {(g,p) =>g.AddV((string) "person").Property((IDictionary) null), (g,p) =>g.V().HasLabel("person").Values()}}, {"g_addV_propertyXemptyX", new List, ITraversal>> {(g,p) =>g.AddV((string) "person").Property(new Dictionary {}), (g,p) =>g.V().HasLabel("person").Values()}}, - {"g_addV_propertyXset_nullX", new List, ITraversal>> {(g,p) =>g.AddV((string) "foo").Property(Cardinality.Set, null), (g,p) =>g.V().HasLabel("foo").Values()}}, + {"g_addV_propertyXset_nullX", new List, ITraversal>> {(g,p) =>g.AddV((string) "foo").Property(Cardinality.Set, (IDictionary) null), (g,p) =>g.V().HasLabel("foo").Values()}}, {"g_addV_propertyXset_emptyX", new List, ITraversal>> {(g,p) =>g.AddV((string) "foo").Property(Cardinality.Set, new Dictionary {}), (g,p) =>g.V().HasLabel("person").Values()}}, {"g_addVXpersonX_propertyXname_joshX_propertyXage_nullX", new List, ITraversal>> {(g,p) =>g.AddV((string) "person").Property("name", "josh").Property("age", null), (g,p) =>g.V().Has("person", "age", (object) null)}}, {"g_addVXpersonX_propertyXname_markoX_propertyXfriendWeight_null_acl_nullX", new List, ITraversal>> {(g,p) =>g.AddV((string) "person").Property("name", "marko").Property("friendWeight", null, "acl", null), (g,p) =>g.V().Has("person", "name", "marko").Has("friendWeight", (object) null), (g,p) =>g.V().Has("person", "name", "marko").Properties("friendWeight").Has("acl", (object) null), (g,p) =>g.V().Has("person", "name", "marko").Properties("friendWeight").Count()}}, @@ -3454,6 +3534,10 @@ private static IDictionary, ITraversal>> {(g,p) =>g.V(p["vid1"]).OutE("knows").BothV()}}, {"g_VX1X_outEXknowsX_bothV_name", new List, ITraversal>> {(g,p) =>g.V(p["vid1"]).OutE("knows").BothV().Values("name")}}, {"g_V_toEXout_knowsvarX_valuesXweightX", new List, ITraversal>> {(g,p) =>g.V().ToE(Direction.Out, new GValue("xx1", (string) p["xx1"])).Values("weight")}}, + {"g_VXvid1X_EXoutE_idX", new List, ITraversal>> {(g,p) =>g.V(p["vid1"]).E(__.OutE().Id())}}, + {"g_VXvid1X_outEXknowsX_hasXinV_name_vadasX_id_EXidentityX", new List, ITraversal>> {(g,p) =>g.V(p["vid1"]).OutE("knows").Filter(__.InV().Has("name", "vadas")).Id().E(__.Identity())}}, + {"g_injectX9999X_EXidentityX", new List, ITraversal>> {(g,p) =>g.Inject(9999).E(__.Identity())}}, + {"g_EXVXvid1X_outE_idX", new List, ITraversal>> {(g,p) =>g.E(__.V(p["vid1"]).OutE().Id())}}, {"g_VX1X_properties_element", new List, ITraversal>> {(g,p) =>g.V(p["vid2"]).Properties().Element().Limit(1)}}, {"g_V_properties_element", new List, ITraversal>> {(g,p) =>g.V().Properties().Element()}}, {"g_V_propertiesXageX_element", new List, ITraversal>> {(g,p) =>g.V().Properties("age").Element()}}, @@ -4118,6 +4202,12 @@ private static IDictionary, ITraversal>> {(g,p) =>g.V().Out("knows").V().Values("name")}}, {"g_V_hasXname_GarciaX_inXsungByX_asXsongX_V_hasXname_Willie_DixonX_inXwrittenByX_whereXeqXsongXX_name", new List, ITraversal>> {(g,p) =>g.V().Has("artist", "name", "Garcia").In("sungBy").As("song").V().Has("artist", "name", "Willie_Dixon").In("writtenBy").Where(P.Eq("song")).Values("name")}}, {"g_V_hasLabelXpersonX_asXpX_VXsoftwareX_addInEXuses_pX", new List, ITraversal>> {(g,p) =>g.AddV((string) "person").Property("name", "marko").Property("age", 29).As("marko").AddV((string) "person").Property("name", "vadas").Property("age", 27).As("vadas").AddV((string) "software").Property("name", "lop").Property("lang", "java").As("lop").AddV((string) "person").Property("name", "josh").Property("age", 32).As("josh").AddV((string) "software").Property("name", "ripple").Property("lang", "java").As("ripple").AddV((string) "person").Property("name", "peter").Property("age", 35).As("peter").AddE((string) "knows").From("marko").To("vadas").Property("weight", 0.5d).AddE((string) "knows").From("marko").To("josh").Property("weight", 1.0d).AddE((string) "created").From("marko").To("lop").Property("weight", 0.4d).AddE((string) "created").From("josh").To("ripple").Property("weight", 1.0d).AddE((string) "created").From("josh").To("lop").Property("weight", 0.4d).AddE((string) "created").From("peter").To("lop").Property("weight", 0.2d), (g,p) =>g.V().HasLabel("person").As("p").V(p["xx1"]).AddE((string) "uses").From("p"), (g,p) =>g.E().HasLabel("uses"), (g,p) =>g.V(p["vid1"]).OutE("uses"), (g,p) =>g.V(p["vid2"]).OutE("uses"), (g,p) =>g.V(p["vid3"]).InE("uses"), (g,p) =>g.V(p["vid4"]).OutE("uses"), (g,p) =>g.V(p["vid5"]).InE("uses"), (g,p) =>g.V(p["vid6"]).OutE("uses")}}, + {"g_VXvid1X_id_VXidentityX_name", new List, ITraversal>> {(g,p) =>g.V(p["vid1"]).Id().V(__.Identity()).Values("name")}}, + {"g_VXvid1_vid2X_id_VXidentityX_name", new List, ITraversal>> {(g,p) =>g.V(p["vid1"], p["vid2"]).Id().V(__.Identity()).Values("name")}}, + {"g_VXvid1X_id_asXbookmarkX_V_hasXname_joshX_VXselectXbookmarkXX_name", new List, ITraversal>> {(g,p) =>g.V(p["vid1"]).Id().As("bookmark").V().Has("name", "josh").V(__.Select("bookmark")).Values("name")}}, + {"g_VXvid1X_VXoutXknowsX_idX_name", new List, ITraversal>> {(g,p) =>g.V(p["vid1"]).V(__.Out("knows").Id()).Values("name")}}, + {"g_injectX9999X_VXidentityX", new List, ITraversal>> {(g,p) =>g.Inject(9999).V(__.Identity())}}, + {"g_VXVXvid1X_idX_name", new List, ITraversal>> {(g,p) =>g.V(__.V(p["vid1"]).Id()).Values("name")}}, {"InjectXnullX_eqXnullX", new List, ITraversal>> {(g,p) =>g.Inject(null).Is(P.Eq(null))}}, {"InjectXnullX_neqXnullX", new List, ITraversal>> {(g,p) =>g.Inject(null).Is(P.Neq(null))}}, {"InjectXnullX_ltXnullX", new List, ITraversal>> {(g,p) =>g.Inject(null).Is(P.Lt(null))}}, @@ -4390,6 +4480,16 @@ private static IDictionary, ITraversal>> {(g,p) =>g.Inject(new List { 1, 3, 100, 300 })}}, {"g_injectX1_3_100_300X_set", new List, ITraversal>> {(g,p) =>g.Inject(new HashSet { 1, 3, 100, 300 })}}, {"g_injectX1_1X_set", new List, ITraversal>> {(g,p) =>g.Inject(new HashSet { 1, 1 })}}, + {"g_VXvid2X_propertyXalias_VXvid1X_valuesXnameXX", new List, ITraversal>> {(g,p) =>g.AddV((string) "person").Property("name", "marko").Property("age", 29).AddV((string) "person").Property("name", "vadas").Property("age", 27).AddV((string) "software").Property("name", "lop").Property("lang", "java").AddV((string) "person").Property("name", "josh").Property("age", 32).AddV((string) "person").Property("name", "peter").Property("age", 35).AddV((string) "software").Property("name", "ripple").Property("lang", "java"), (g,p) =>g.V(p["vid2"]).Property("alias", __.V(p["vid1"]).Values("name")), (g,p) =>g.V().Has("alias", "marko")}}, + {"g_VXvid1X_propertyXcreatorCount_VXvid1X_inXcreatedX_countX", new List, ITraversal>> {(g,p) =>g.AddV((string) "person").Property("name", "marko").Property("age", 29).As("marko").AddV((string) "person").Property("name", "vadas").Property("age", 27).AddV((string) "software").Property("name", "lop").Property("lang", "java").As("lop").AddV((string) "person").Property("name", "josh").Property("age", 32).As("josh").AddV((string) "person").Property("name", "peter").Property("age", 35).As("peter").AddV((string) "software").Property("name", "ripple").Property("lang", "java").As("ripple").AddE((string) "knows").From("marko").To(__.V().Has("name", "vadas")).AddE((string) "knows").From("marko").To("josh").AddE((string) "created").From("marko").To("lop").AddE((string) "created").From("josh").To("lop").AddE((string) "created").From("josh").To("ripple").AddE((string) "created").From("peter").To("lop"), (g,p) =>g.V(p["vid1"]).Property("creatorCount", __.V(p["vid1"]).In("created").Count()), (g,p) =>g.V().Has("creatorCount", 3l)}}, + {"g_VXvid1X_propertyXknownCount_outXknowsX_countX", new List, ITraversal>> {(g,p) =>g.AddV((string) "person").Property("name", "marko").Property("age", 29).As("marko").AddV((string) "person").Property("name", "vadas").Property("age", 27).As("vadas").AddV((string) "software").Property("name", "lop").Property("lang", "java").AddV((string) "person").Property("name", "josh").Property("age", 32).As("josh").AddV((string) "person").Property("name", "peter").Property("age", 35).AddV((string) "software").Property("name", "ripple").Property("lang", "java").AddE((string) "knows").From("marko").To("vadas").AddE((string) "knows").From("marko").To("josh"), (g,p) =>g.V(p["vid1"]).Property("knownCount", __.Out("knows").Count()), (g,p) =>g.V().Has("knownCount", 2l)}}, + {"g_VXvid1X_propertyXcreator_inXcreatedX_valuesXnameXX", new List, ITraversal>> {(g,p) =>g.AddV((string) "person").Property("name", "marko").Property("age", 29).As("marko").AddV((string) "person").Property("name", "vadas").Property("age", 27).AddV((string) "software").Property("name", "lop").Property("lang", "java").As("lop").AddV((string) "person").Property("name", "josh").Property("age", 32).As("josh").AddV((string) "person").Property("name", "peter").Property("age", 35).As("peter").AddV((string) "software").Property("name", "ripple").Property("lang", "java").As("ripple").AddE((string) "knows").From("marko").To(__.V().Has("name", "vadas")).AddE((string) "knows").From("marko").To("josh").AddE((string) "created").From("marko").To("lop").AddE((string) "created").From("josh").To("lop").AddE((string) "created").From("josh").To("ripple").AddE((string) "created").From("peter").To("lop"), (g,p) =>g.V(p["vid1"]).Property("creator", __.In("created").Values("name")), (g,p) =>g.V().Has("creator")}}, + {"g_VXvid2X_propertyXVXvid1X_projectXfriendCount_softwareCreatedXX", new List, ITraversal>> {(g,p) =>g.AddV((string) "person").Property("name", "marko").Property("age", 29).As("marko").AddV((string) "person").Property("name", "vadas").Property("age", 27).As("vadas").AddV((string) "software").Property("name", "lop").Property("lang", "java").As("lop").AddV((string) "person").Property("name", "josh").Property("age", 32).As("josh").AddV((string) "person").Property("name", "peter").Property("age", 35).As("peter").AddV((string) "software").Property("name", "ripple").Property("lang", "java").As("ripple").AddE((string) "knows").From("marko").To("vadas").AddE((string) "knows").From("marko").To("josh").AddE((string) "created").From("marko").To("lop").AddE((string) "created").From("josh").To("lop").AddE((string) "created").From("josh").To("ripple").AddE((string) "created").From("peter").To("lop"), (g,p) =>g.V(p["vid2"]).Property((ITraversal) __.V(p["vid1"]).Project("friendCount", "softwareCreated").By(__.Out("knows").Count()).By(__.Out("created").Values("name"))), (g,p) =>g.V().Has("friendCount", 2l), (g,p) =>g.V().Has("softwareCreated", "lop")}}, + {"g_VXvid2X_propertyXVXvid1X_projectXoriginalName_originalLabelX_byXnameX_byXlabelXX", new List, ITraversal>> {(g,p) =>g.AddV((string) "person").Property("name", "marko").Property("age", 29).As("marko").AddV((string) "person").Property("name", "vadas").Property("age", 27).AddV((string) "software").Property("name", "lop").Property("lang", "java").As("lop").AddV((string) "person").Property("name", "josh").Property("age", 32).AddV((string) "person").Property("name", "peter").Property("age", 35).AddV((string) "software").Property("name", "ripple").Property("lang", "java"), (g,p) =>g.V(p["vid2"]).Property((ITraversal) __.V(p["vid1"]).Project("originalName", "originalLabel").By(__.Values("name")).By(__.Label())), (g,p) =>g.V(p["vid2"]).Has("originalName", "marko"), (g,p) =>g.V(p["vid2"]).Has("originalLabel", "person")}}, + {"g_VXvid1X_propertyXlist_friends_outXknowsX_valuesXnameXX", new List, ITraversal>> {(g,p) =>g.AddV((string) "person").Property("name", "marko").Property("age", 29).As("marko").AddV((string) "person").Property("name", "vadas").Property("age", 27).As("vadas").AddV((string) "software").Property("name", "lop").Property("lang", "java").AddV((string) "person").Property("name", "josh").Property("age", 32).As("josh").AddV((string) "person").Property("name", "peter").Property("age", 35).AddV((string) "software").Property("name", "ripple").Property("lang", "java").AddE((string) "knows").From("marko").To("vadas").AddE((string) "knows").From("marko").To("josh"), (g,p) =>g.V(p["vid1"]).Property(Cardinality.List, "friends", __.Out("knows").Values("name")), (g,p) =>g.V(p["vid1"]).Properties("friends")}}, + {"g_VXvid1X_propertyXset_langs_outXcreatedX_valuesXlangXX", new List, ITraversal>> {(g,p) =>g.AddV((string) "person").Property("name", "marko").Property("age", 29).AddV((string) "person").Property("name", "vadas").Property("age", 27).AddV((string) "software").Property("name", "lop").Property("lang", "java").As("lop").AddV((string) "person").Property("name", "josh").Property("age", 32).As("josh").AddV((string) "person").Property("name", "peter").Property("age", 35).AddV((string) "software").Property("name", "ripple").Property("lang", "java").As("ripple").AddE((string) "created").From("josh").To("lop").AddE((string) "created").From("josh").To("ripple"), (g,p) =>g.V(p["vid1"]).Property(Cardinality.Set, "langs", __.Out("created").Values("lang")), (g,p) =>g.V(p["vid1"]).Properties("langs")}}, + {"g_VXvid1X_propertyXsingle_friend_outXknowsX_valuesXnameXX", new List, ITraversal>> {(g,p) =>g.AddV((string) "person").Property("name", "marko").Property("age", 29).As("marko").AddV((string) "person").Property("name", "vadas").Property("age", 27).As("vadas").AddV((string) "software").Property("name", "lop").Property("lang", "java").AddV((string) "person").Property("name", "josh").Property("age", 32).As("josh").AddV((string) "person").Property("name", "peter").Property("age", 35).AddV((string) "software").Property("name", "ripple").Property("lang", "java").AddE((string) "knows").From("marko").To("vadas").AddE((string) "knows").From("marko").To("josh"), (g,p) =>g.V(p["vid1"]).Property(Cardinality.Single, "friend", __.Out("knows").Values("name"))}}, + {"g_VXvid1X_propertyXVXvid1X_groupX_byXidX_byXvaluesXnameXXX", new List, ITraversal>> {(g,p) =>g.AddV((string) "person").Property("name", "marko").Property("age", 29), (g,p) =>g.V(p["vid1"]).Property((ITraversal) __.V(p["vid1"]).Group().By(__.Id()).By(__.Values("name")))}}, {"g_io_readXkryoX", new List, ITraversal>> {(g,p) =>g.Io("data/tinkerpop-modern.kryo").Read(), (g,p) =>g.V(), (g,p) =>g.E()}}, {"g_io_read_withXreader_gryoX", new List, ITraversal>> {(g,p) =>g.Io("data/tinkerpop-modern.kryo").With(IO.Reader, IO.Gryo).Read(), (g,p) =>g.V(), (g,p) =>g.E()}}, {"g_io_readXgraphsonX", new List, ITraversal>> {(g,p) =>g.Io("data/tinkerpop-modern.json").Read(), (g,p) =>g.V(), (g,p) =>g.E()}}, diff --git a/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/Channelizer.java b/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/Channelizer.java index bab50ede53c..04cea7823d1 100644 --- a/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/Channelizer.java +++ b/gremlin-driver/src/main/java/org/apache/tinkerpop/gremlin/driver/Channelizer.java @@ -29,6 +29,9 @@ import io.netty.handler.ssl.SslContext; import io.netty.handler.ssl.SslHandler; import io.netty.handler.proxy.HttpProxyHandler; + +import java.net.InetSocketAddress; +import java.net.SocketAddress; import io.netty.handler.timeout.IdleStateHandler; import org.apache.tinkerpop.gremlin.driver.exception.ConnectionException; import org.apache.tinkerpop.gremlin.driver.handler.GremlinResponseHandler; @@ -147,9 +150,10 @@ protected void initChannel(final SocketChannel socketChannel) { // TLS handshake. final ProxyOptions proxy = cluster.getProxy(); if (proxy != null) { + final SocketAddress proxyAddress = resolveProxyAddress(proxy.getAddress()); final HttpProxyHandler proxyHandler = proxy.hasCredentials() - ? new HttpProxyHandler(proxy.getAddress(), proxy.getUsername(), proxy.getPassword()) - : new HttpProxyHandler(proxy.getAddress()); + ? new HttpProxyHandler(proxyAddress, proxy.getUsername(), proxy.getPassword()) + : new HttpProxyHandler(proxyAddress); pipeline.addLast(PIPELINE_PROXY_HANDLER, proxyHandler); } @@ -189,6 +193,21 @@ public void connected() { } } } + + /** + * Resolves an unresolved proxy {@link SocketAddress} so that Netty's {@code HttpProxyHandler} can connect to + * it. A resolved address (or a non-{@link InetSocketAddress}) is returned unchanged. + */ + private static SocketAddress resolveProxyAddress(final SocketAddress address) { + if (address instanceof InetSocketAddress) { + final InetSocketAddress inet = (InetSocketAddress) address; + if (inet.isUnresolved()) { + return new InetSocketAddress(inet.getHostString(), inet.getPort()); + } + } + return address; + } + } /** diff --git a/gremlin-driver/src/test/java/org/apache/tinkerpop/gremlin/driver/ProxyBehaviorIntegrateTest.java b/gremlin-driver/src/test/java/org/apache/tinkerpop/gremlin/driver/ProxyBehaviorIntegrateTest.java new file mode 100644 index 00000000000..0da00b9d530 --- /dev/null +++ b/gremlin-driver/src/test/java/org/apache/tinkerpop/gremlin/driver/ProxyBehaviorIntegrateTest.java @@ -0,0 +1,143 @@ +/* + * 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.tinkerpop.gremlin.driver; + +import org.apache.tinkerpop.gremlin.socket.server.RecordingProxyServer; +import org.apache.tinkerpop.gremlin.socket.server.SimpleTestServer; +import org.apache.tinkerpop.gremlin.socket.server.SocketServerConstants; +import org.apache.tinkerpop.gremlin.socket.server.TestHttpServerInitializer; +import org.apache.tinkerpop.gremlin.structure.Vertex; +import org.apache.tinkerpop.gremlin.util.ser.GraphBinaryMessageSerializerV4; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.List; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.IsInstanceOf.instanceOf; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +/** + * Proves that {@link Cluster.Builder#proxy(ProxyOptions)} routes driver traffic through an HTTP proxy. + *

+ * A {@link SimpleTestServer} (the real Gremlin socket server) runs on {@link SocketServerConstants#PORT} + * and an in-process {@link RecordingProxyServer} runs on {@link SocketServerConstants#PROXY_PORT}. The + * proxy records the {@code host:port} target of every tunnel it establishes and exposes a small control + * API ({@code GET /__recorded}, {@code POST /__reset}) that the tests query with the JDK11 + * {@link java.net.http.HttpClient}. + */ +public class ProxyBehaviorIntegrateTest { + + private static final int PORT = SocketServerConstants.PORT; + private static final int PROXY_PORT = SocketServerConstants.PROXY_PORT; + + private static SimpleTestServer server; + private static RecordingProxyServer proxyServer; + private static HttpClient httpClient; + + @BeforeClass + public static void setUp() throws InterruptedException { + server = new SimpleTestServer(PORT); + server.start(new TestHttpServerInitializer()); + + proxyServer = new RecordingProxyServer(PROXY_PORT); + proxyServer.start(); + + httpClient = HttpClient.newHttpClient(); + } + + @AfterClass + public static void tearDown() { + if (server != null) server.stop(); + if (proxyServer != null) proxyServer.stop(); + } + + private static Cluster.Builder buildCluster() { + return Cluster.build("localhost") + .validationRequest(SocketServerConstants.GREMLIN_SINGLE_VERTEX) + .port(PORT) + .serializer(new GraphBinaryMessageSerializerV4()); + } + + private static String getRecorded() throws Exception { + final HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create("http://localhost:" + PROXY_PORT + "/__recorded")) + .GET() + .build(); + return httpClient.send(request, HttpResponse.BodyHandlers.ofString()).body(); + } + + private static void resetRecorded() throws Exception { + final HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create("http://localhost:" + PROXY_PORT + "/__reset")) + .POST(HttpRequest.BodyPublishers.noBody()) + .build(); + httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + } + + @Test + public void shouldRouteTrafficThroughProxy() throws Exception { + resetRecorded(); + + final Cluster cluster = buildCluster() + .proxy(ProxyOptions.create("localhost", PROXY_PORT)) + .create(); + try { + final Client client = cluster.connect(); + final List results = client.submit(SocketServerConstants.GREMLIN_SINGLE_VERTEX).all().get(); + assertEquals(1, results.size()); + assertThat(results.get(0).getObject(), instanceOf(Vertex.class)); + + // the driver issues an HTTP CONNECT, so the proxy should have recorded the tunnel to the + // socket server. Match on the ":45943" substring to stay tolerant of the host form. + final String recorded = getRecorded(); + assertTrue("Expected the proxy to have recorded the tunnel to :" + PORT + " but was: " + recorded, + recorded.contains(":" + PORT)); + } finally { + cluster.close(); + } + } + + @Test + public void shouldNotRecordWithoutProxy() throws Exception { + resetRecorded(); + + final Cluster cluster = buildCluster().create(); + try { + final Client client = cluster.connect(); + final List results = client.submit(SocketServerConstants.GREMLIN_SINGLE_VERTEX).all().get(); + assertEquals(1, results.size()); + assertThat(results.get(0).getObject(), instanceOf(Vertex.class)); + + // without a proxy configured the driver connects directly, so nothing should be recorded. + final String recorded = getRecorded(); + assertFalse("Expected no tunnel to :" + PORT + " to be recorded but was: " + recorded, + recorded.contains(":" + PORT)); + } finally { + cluster.close(); + } + } +} diff --git a/gremlin-go/docker-compose.yml b/gremlin-go/docker-compose.yml index 8966ed0bffa..91cf67e30cf 100644 --- a/gremlin-go/docker-compose.yml +++ b/gremlin-go/docker-compose.yml @@ -50,6 +50,7 @@ services: - SOCKET_SERVER_VERSION=${GREMLIN_SERVER} ports: - "45943:45943" + - "45944:45944" gremlin-go-integration-tests: container_name: gremlin-go-integration-tests @@ -63,6 +64,7 @@ services: - GREMLIN_SERVER_URL=http://gremlin-server-test:45940/gremlin - GREMLIN_SERVER_BASIC_AUTH_URL=https://gremlin-server-test:45941/gremlin - GREMLIN_SOCKET_SERVER_URL=http://gremlin-socket-server-test-go:45943/gremlin + - GREMLIN_SOCKET_SERVER_PROXY_URL=http://gremlin-socket-server-test-go:45944 - RUN_INTEGRATION_TESTS=true - RUN_INTEGRATION_WITH_ALIAS_TESTS=true - RUN_BASIC_AUTH_INTEGRATION_TESTS=true diff --git a/gremlin-go/driver/client_behavior_test.go b/gremlin-go/driver/client_behavior_test.go index f274c0ad2c5..b358b064041 100644 --- a/gremlin-go/driver/client_behavior_test.go +++ b/gremlin-go/driver/client_behavior_test.go @@ -21,7 +21,11 @@ package gremlingo import ( "context" + "encoding/json" "fmt" + "net/http" + "net/url" + "strings" "sync" "testing" "time" @@ -35,6 +39,105 @@ func socketServerURL() string { fmt.Sprintf("http://localhost:%d/gremlin", socketServerPort)) } +func socketServerProxyURL() string { + return getEnvOrDefaultString("GREMLIN_SOCKET_SERVER_PROXY_URL", + "http://localhost:45944") +} + +// resetProxyRecording clears the proxy's recorded targets via POST /__reset. +func resetProxyRecording(t *testing.T) { + t.Helper() + proxyURL := socketServerProxyURL() + resp, err := http.Post(proxyURL+"/__reset", "application/json", nil) + if err != nil { + t.Skipf("Recording proxy not available: %v", err) + } + resp.Body.Close() +} + +// recordedProxyTargets fetches the recorded "host:port" targets via GET /__recorded. +func recordedProxyTargets(t *testing.T) []string { + t.Helper() + proxyURL := socketServerProxyURL() + resp, err := http.Get(proxyURL + "/__recorded") + if err != nil { + t.Skipf("Recording proxy not available: %v", err) + } + defer resp.Body.Close() + + var recorded []string + if err := json.NewDecoder(resp.Body).Decode(&recorded); err != nil { + t.Fatalf("failed to decode recorded proxy targets: %v", err) + } + return recorded +} + +// anyTargetHasPortSuffix reports whether any recorded target ends with ":", +// matching on port suffix only so the host portion is ignored. +func anyTargetHasPortSuffix(targets []string, port int) bool { + suffix := fmt.Sprintf(":%d", port) + for _, target := range targets { + if strings.HasSuffix(target, suffix) { + return true + } + } + return false +} + +// TestProxyRoutesSocketServerTraffic verifies that traffic submitted through a +// client configured with a Proxy is routed to the socket server via the shared +// recording proxy, and that the proxy records the socket server target. +func TestProxyRoutesSocketServerTraffic(t *testing.T) { + resetProxyRecording(t) + + parsedProxyURL, err := url.Parse(socketServerProxyURL()) + require.NoError(t, err) + + client, err := NewClient(socketServerURL(), func(settings *ClientSettings) { + settings.Proxy = func(*http.Request) (*url.URL, error) { + return parsedProxyURL, nil + } + }) + require.NoError(t, err) + defer client.Close() + + rs, err := client.Submit(gremlinSingleVertex) + require.NoError(t, err) + results, err := rs.All() + require.NoError(t, err) + assert.Equal(t, 1, len(results)) + + targets := recordedProxyTargets(t) + assert.True(t, anyTargetHasPortSuffix(targets, socketServerPort), + "expected at least one recorded target ending with :%d, got %v", socketServerPort, targets) +} + +// TestProxyNegativeControl verifies that a client connecting directly to the +// socket server (no Proxy configured) does not route through the recording +// proxy, so the proxy records no socket server target. +func TestProxyNegativeControl(t *testing.T) { + resetProxyRecording(t) + + client, err := NewClient(socketServerURL(), func(settings *ClientSettings) { + // Force a direct connection (no proxy) regardless of ambient proxy env vars. + settings.Proxy = func(*http.Request) (*url.URL, error) { + return nil, nil + } + }) + require.NoError(t, err) + defer client.Close() + + rs, err := client.Submit(gremlinSingleVertex) + require.NoError(t, err) + results, err := rs.All() + require.NoError(t, err) + assert.Equal(t, 1, len(results)) + + targets := recordedProxyTargets(t) + assert.False(t, anyTargetHasPortSuffix(targets, socketServerPort), + "expected no recorded target ending with :%d, got %v", socketServerPort, targets) +} + func newSocketServerClient(t *testing.T, configurations ...func(*ClientSettings)) *Client { t.Helper() url := socketServerURL() diff --git a/gremlin-js/gremlin-javascript/docker-compose.yml b/gremlin-js/gremlin-javascript/docker-compose.yml index 1c00082634c..2505109bbf0 100644 --- a/gremlin-js/gremlin-javascript/docker-compose.yml +++ b/gremlin-js/gremlin-javascript/docker-compose.yml @@ -50,6 +50,7 @@ services: - SOCKET_SERVER_VERSION=${GREMLIN_SERVER} ports: - "45943:45943" + - "45944:45944" gremlin-js-integration-tests: container_name: gremlin-js-integration-tests @@ -66,6 +67,7 @@ services: - DOCKER_ENVIRONMENT=true - GREMLIN_SERVER_URL=http://gremlin-server-test-js:45940/gremlin - GREMLIN_SOCKET_SERVER_URL=http://gremlin-socket-server-test-js:45943/gremlin + - GREMLIN_SOCKET_SERVER_PROXY_URL=http://gremlin-socket-server-test-js:45944 - VERTEX_LABEL=javascript-example - IO_TEST_DIRECTORY=/workspace/gremlin-js/gremlin-javascript/gremlin-test/graphbinary/ - NPM_CONFIG_CACHE=/tmp/npm-cache diff --git a/gremlin-js/gremlin-javascript/test/integration/proxy-behavior-tests.js b/gremlin-js/gremlin-javascript/test/integration/proxy-behavior-tests.js new file mode 100644 index 00000000000..7d410c41fe5 --- /dev/null +++ b/gremlin-js/gremlin-javascript/test/integration/proxy-behavior-tests.js @@ -0,0 +1,52 @@ +/* + * 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. + */ + +import assert from 'assert'; + +import Client from '../../lib/driver/client.js'; + +import { GREMLIN_SINGLE_VERTEX } from './socket-server-constants.js'; + +const url = process.env.GREMLIN_SOCKET_SERVER_URL || 'http://localhost:45943/gremlin'; +const proxyUrl = process.env.GREMLIN_SOCKET_SERVER_PROXY_URL || 'http://localhost:45944'; + +describe('proxy behavior', function () { + this.timeout(30000); + + it('routes socket server traffic through the configured proxy', async function () { + await fetch(proxyUrl + '/__reset', { method: 'POST' }); + const client = new Client(url, { traversalSource: 'g', proxy: proxyUrl }); + await client.open?.(); + const result = await client.submit(GREMLIN_SINGLE_VERTEX); + assert.strictEqual(result.length, 1); + const recorded = await (await fetch(proxyUrl + '/__recorded')).json(); + assert.ok(recorded.some((t) => t.endsWith(':45943')), JSON.stringify(recorded)); + await client.close(); + }); + + it('does not record when no proxy is configured', async function () { + await fetch(proxyUrl + '/__reset', { method: 'POST' }); + const client = new Client(url, { traversalSource: 'g' }); + const result = await client.submit(GREMLIN_SINGLE_VERTEX); + assert.strictEqual(result.length, 1); + const recorded = await (await fetch(proxyUrl + '/__recorded')).json(); + assert.ok(!recorded.some((t) => t.endsWith(':45943'))); + await client.close(); + }); +}); diff --git a/gremlin-python/docker-compose.yml b/gremlin-python/docker-compose.yml index cdb4cbfdcc5..0eee3f2a5dd 100644 --- a/gremlin-python/docker-compose.yml +++ b/gremlin-python/docker-compose.yml @@ -51,6 +51,7 @@ services: - SOCKET_SERVER_VERSION=${GREMLIN_SERVER} ports: - "45943:45943" + - "45944:45944" gremlin-python-integration-tests: container_name: gremlin-python-integration-tests @@ -65,6 +66,7 @@ services: - GREMLIN_SERVER_URL=http://gremlin-server-test-python:{}/gremlin - GREMLIN_SERVER_BASIC_AUTH_URL=https://gremlin-server-test-python:{}/gremlin - GREMLIN_SOCKET_SERVER_URL=http://gremlin-socket-server-test-python:45943/gremlin + - GREMLIN_SOCKET_SERVER_PROXY_URL=http://gremlin-socket-server-test-python:45944 - IO_TEST_DIRECTORY=/python_app/gremlin-test/graphbinary/ - PYTEST_ARGS=${PYTEST_ARGS:-} - RADISH_ARGS=${RADISH_ARGS:-} diff --git a/gremlin-python/src/main/python/tests/integration/driver/test_client_behavior.py b/gremlin-python/src/main/python/tests/integration/driver/test_client_behavior.py index 013280e39fc..18eef5edf8e 100644 --- a/gremlin-python/src/main/python/tests/integration/driver/test_client_behavior.py +++ b/gremlin-python/src/main/python/tests/integration/driver/test_client_behavior.py @@ -18,8 +18,10 @@ # import asyncio +import json import os import time +import urllib.request from concurrent.futures import ThreadPoolExecutor, as_completed import pytest @@ -43,6 +45,7 @@ ) url = os.environ.get('GREMLIN_SOCKET_SERVER_URL', 'http://localhost:{}/gremlin'.format(PORT)) +proxy_url = os.environ.get('GREMLIN_SOCKET_SERVER_PROXY_URL', 'http://localhost:45944') @pytest.fixture(scope="module") @@ -50,7 +53,8 @@ def socket_server_client(): try: client = Client(url, 'g') # Verify connectivity - client.submit(GREMLIN_SINGLE_VERTEX).all().result() + result = client.submit(GREMLIN_SINGLE_VERTEX).all().result() + assert len(result) == 1 except Exception: pytest.skip("Socket server is not available at {}".format(url)) yield client @@ -195,3 +199,34 @@ def submit_close(): assert len(vertex_results) == 5 assert len(close_errors) == 5 + + +def test_proxy_routes_socket_server_traffic(): + # Reset the recording proxy, route a client through it, and confirm the + # proxy observed traffic to the socket server on port 45943. + urllib.request.urlopen(urllib.request.Request(proxy_url + '/__reset', method='POST')) + + client = Client(url, 'g', proxy=proxy_url) + try: + result = client.submit(GREMLIN_SINGLE_VERTEX).all().result() + assert len(result) == 1 + + recorded = json.loads(urllib.request.urlopen(proxy_url + '/__recorded').read().decode()) + assert any(target.endswith(':45943') for target in recorded) + finally: + client.close() + + +def test_proxy_negative_control(): + # Negative control: without the proxy kwarg the client connects directly, + # so the recording proxy should observe no traffic to the socket server. + urllib.request.urlopen(urllib.request.Request(proxy_url + '/__reset', method='POST')) + + client = Client(url, 'g') + try: + client.submit(GREMLIN_SINGLE_VERTEX).all().result() + + recorded = json.loads(urllib.request.urlopen(proxy_url + '/__recorded').read().decode()) + assert not any(target.endswith(':45943') for target in recorded) + finally: + client.close() diff --git a/gremlin-tools/gremlin-socket-server/Dockerfile b/gremlin-tools/gremlin-socket-server/Dockerfile index f40c91a66d8..370281c5a6d 100644 --- a/gremlin-tools/gremlin-socket-server/Dockerfile +++ b/gremlin-tools/gremlin-socket-server/Dockerfile @@ -32,5 +32,6 @@ COPY ${SOCKET_SERVER_DIR}/libs/ /opt/gremlin-socket-server/libs/ WORKDIR /opt/gremlin-socket-server EXPOSE 45943 +EXPOSE 45944 ENTRYPOINT ["java","-jar","gremlin-socket-server.jar"] diff --git a/gremlin-tools/gremlin-socket-server/pom.xml b/gremlin-tools/gremlin-socket-server/pom.xml index 5d25aae750d..42bb46df1fc 100644 --- a/gremlin-tools/gremlin-socket-server/pom.xml +++ b/gremlin-tools/gremlin-socket-server/pom.xml @@ -27,6 +27,23 @@ netty-all ${netty.version} + + io.github.littleproxy + littleproxy + 2.8.0 + + + + io.netty + * + + + org.slf4j diff --git a/gremlin-tools/gremlin-socket-server/src/main/java/org/apache/tinkerpop/gremlin/socket/server/RecordingProxyServer.java b/gremlin-tools/gremlin-socket-server/src/main/java/org/apache/tinkerpop/gremlin/socket/server/RecordingProxyServer.java new file mode 100644 index 00000000000..78f1899ab62 --- /dev/null +++ b/gremlin-tools/gremlin-socket-server/src/main/java/org/apache/tinkerpop/gremlin/socket/server/RecordingProxyServer.java @@ -0,0 +1,191 @@ +/* + * 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.tinkerpop.gremlin.socket.server; + +import io.netty.buffer.Unpooled; +import io.netty.handler.codec.http.DefaultFullHttpResponse; +import io.netty.handler.codec.http.FullHttpResponse; +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpMethod; +import io.netty.handler.codec.http.HttpObject; +import io.netty.handler.codec.http.HttpRequest; +import io.netty.handler.codec.http.HttpResponseStatus; +import org.littleshoot.proxy.HttpFiltersAdapter; +import org.littleshoot.proxy.HttpFiltersSourceAdapter; +import org.littleshoot.proxy.HttpProxyServer; +import org.littleshoot.proxy.impl.DefaultHttpProxyServer; + +import java.net.InetSocketAddress; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1; + +/** + * A proxy used for cross-GLV proxy testing, backed by the LittleProxy library. It records the + * {@code host:port} target of every request it proxies and exposes a small control API so tests can + * prove that traffic actually transited the proxy. + *

+ * Two categories of requests are handled by the installed {@code HttpFilters}: + *

    + *
  • Proxied requests (HTTP CONNECT tunnels used by Java/Netty and JS/undici clients, plus + * absolute-URI forward requests used by Go/Python/.NET for http targets): the target authority is + * recorded and LittleProxy is allowed to proxy the request normally.
  • + *
  • Origin-form control requests served on the same proxy port: {@code GET /__recorded} + * returns a JSON array of the recorded targets, {@code POST /__reset} clears the recorded list. Any + * other control path returns 404. These are short-circuited by returning a response directly from + * the filter so they never leave the proxy.
  • + *
+ */ +public class RecordingProxyServer { + + private final int port; + private final List recordedTargets = new CopyOnWriteArrayList<>(); + private HttpProxyServer proxyServer; + + public RecordingProxyServer(final int port) { + this.port = port; + } + + public void start() { + proxyServer = DefaultHttpProxyServer.bootstrap() + .withAddress(new InetSocketAddress(port)) + .withFiltersSource(new HttpFiltersSourceAdapter() { + @Override + public org.littleshoot.proxy.HttpFilters filterRequest(final HttpRequest originalRequest) { + return new RecordingFilters(originalRequest, recordedTargets); + } + }) + .start(); + } + + public void stop() { + if (proxyServer != null) { + proxyServer.stop(); + } + } + + /** + * Records proxied targets and short-circuits the origin-form control API. + */ + private static class RecordingFilters extends HttpFiltersAdapter { + + private final List recordedTargets; + + RecordingFilters(final HttpRequest originalRequest, final List recordedTargets) { + super(originalRequest); + this.recordedTargets = recordedTargets; + } + + @Override + public io.netty.handler.codec.http.HttpResponse clientToProxyRequest(final HttpObject httpObject) { + if (!(httpObject instanceof HttpRequest)) { + return null; + } + + final HttpRequest request = (HttpRequest) httpObject; + final String uri = request.uri(); + + if (uri.startsWith("/")) { + // origin-form -> control API, short-circuit with a direct response. + return handleControl(request); + } + + // otherwise it is a proxied request: record the authority and let LittleProxy proxy it. + final String authority = extractAuthority(request); + if (authority != null) { + recordedTargets.add(authority); + } + return null; + } + + /** + * Extracts the {@code host:port} authority from a proxied request. For CONNECT the uri is + * already {@code host:port}; for an absolute-URI request the uri is parsed with + * {@link URI}, falling back to the Host header when the uri cannot be parsed. + */ + private String extractAuthority(final HttpRequest request) { + final String uri = request.uri(); + if (HttpMethod.CONNECT.equals(request.method())) { + return uri; + } + + try { + final URI parsed = new URI(uri); + final String host = parsed.getHost(); + if (host != null) { + final int parsedPort = parsed.getPort(); + return host + ":" + (parsedPort == -1 ? 80 : parsedPort); + } + } catch (Exception ignored) { + // fall through to the Host header fallback below + } + + final String hostHeader = request.headers().get(HttpHeaderNames.HOST); + if (hostHeader != null && !hostHeader.isEmpty()) { + return hostHeader.contains(":") ? hostHeader : hostHeader + ":80"; + } + return null; + } + + private FullHttpResponse handleControl(final HttpRequest request) { + final String uri = request.uri(); + if (HttpMethod.GET.equals(request.method()) && "/__recorded".equals(uri)) { + return buildResponse(HttpResponseStatus.OK, toJsonArray(recordedTargets), "application/json"); + } else if (HttpMethod.POST.equals(request.method()) && "/__reset".equals(uri)) { + recordedTargets.clear(); + return buildResponse(HttpResponseStatus.OK, "", "text/plain"); + } else { + return buildResponse(HttpResponseStatus.NOT_FOUND, "", "text/plain"); + } + } + + private static FullHttpResponse buildResponse(final HttpResponseStatus status, final String body, + final String contentType) { + final FullHttpResponse response = new DefaultFullHttpResponse( + HTTP_1_1, status, Unpooled.copiedBuffer(body, StandardCharsets.UTF_8)); + response.headers().set(HttpHeaderNames.CONTENT_TYPE, contentType + "; charset=UTF-8"); + response.headers().set(HttpHeaderNames.CONTENT_LENGTH, response.content().readableBytes()); + return response; + } + + /** + * Builds a JSON array of strings by hand to avoid pulling in a JSON dependency. + */ + private static String toJsonArray(final List targets) { + final StringBuilder sb = new StringBuilder("["); + final List snapshot = new ArrayList<>(targets); + for (int i = 0; i < snapshot.size(); i++) { + if (i > 0) { + sb.append(","); + } + sb.append("\"").append(escapeJson(snapshot.get(i))).append("\""); + } + sb.append("]"); + return sb.toString(); + } + + private static String escapeJson(final String value) { + return value.replace("\\", "\\\\").replace("\"", "\\\""); + } + } +} diff --git a/gremlin-tools/gremlin-socket-server/src/main/java/org/apache/tinkerpop/gremlin/socket/server/SocketServerConstants.java b/gremlin-tools/gremlin-socket-server/src/main/java/org/apache/tinkerpop/gremlin/socket/server/SocketServerConstants.java index 74289110208..a276a75704e 100644 --- a/gremlin-tools/gremlin-socket-server/src/main/java/org/apache/tinkerpop/gremlin/socket/server/SocketServerConstants.java +++ b/gremlin-tools/gremlin-socket-server/src/main/java/org/apache/tinkerpop/gremlin/socket/server/SocketServerConstants.java @@ -26,6 +26,8 @@ public final class SocketServerConstants { public static final int PORT = 45943; + public static final int PROXY_PORT = 45944; + public static final String GREMLIN_SINGLE_VERTEX = "server_single_vertex"; public static final String GREMLIN_CLOSE_CONNECTION = "server_close_connection"; public static final String GREMLIN_VERTEX_THEN_CLOSE = "server_vertex_then_close"; diff --git a/gremlin-tools/gremlin-socket-server/src/main/java/org/apache/tinkerpop/gremlin/socket/server/SocketServerRunner.java b/gremlin-tools/gremlin-socket-server/src/main/java/org/apache/tinkerpop/gremlin/socket/server/SocketServerRunner.java index 5f6d958cc0c..15609808c4c 100644 --- a/gremlin-tools/gremlin-socket-server/src/main/java/org/apache/tinkerpop/gremlin/socket/server/SocketServerRunner.java +++ b/gremlin-tools/gremlin-socket-server/src/main/java/org/apache/tinkerpop/gremlin/socket/server/SocketServerRunner.java @@ -28,6 +28,10 @@ public class SocketServerRunner { public static void main(final String[] args) throws InterruptedException { final SimpleTestServer server = new SimpleTestServer(SocketServerConstants.PORT); final Channel channel = server.start(new TestHttpServerInitializer()); + + final RecordingProxyServer proxyServer = new RecordingProxyServer(SocketServerConstants.PROXY_PORT); + proxyServer.start(); + while (channel.isOpen()) { Thread.sleep(1000); }