Skip to content

Commit

Permalink
Ability to override discoverable bolt URI
Browse files Browse the repository at this point in the history
This introduces a minor overhaul of the discovery
service, allowing arbitrary URLs to be published to it.
That functionality is leveraged to have the enterprise
edition of the server publish a `bolt+routing` URI.

--

This has two elements that will make it confusing - the term
"discovery", and the idea of a "public" URI for Bolt.

"Discovery" in Neo could refer both to the discovery *protocol*,
used by causal clustering, or to REST API Discovery, the idea
that the HTTP API describes its own capabilities. This PR is
exclusively about REST/HTTP discovery - it is adding a capability
to the JSON document a user recieves when she does:

    GET http://localhost:7474

The "public" URI portion is more complex, and is the core of what
this PR is mucking about in. Currently, you can find a Bolt URL
to a Neo4j instance in two ways - by asking the HTTP discovery
endpoint, or by asking the routing table procedure. Currently,
both of these methods will give you URLs ultimately originating
from the configuration of the `advertised_address` on the bolt
connector.

However, they serve two different purposes. The routing table is
used *internally* by Bolt Drivers, providing addresses to connect
to exact and specific Neo4j instances.

The Discovery URL is used *externally*, as the root URL to give to
a driver. By connecting via HTTP and retrieving the discovery
document, tools like the browser find out the appropriate URL
to pass when creating a Bolt Driver.

Hence - a sensible "Advertised" URL is always a "bolt://..." URL
pointing to the specific instance being configured. However, a
sensible "Discoverable" URL is sometimes different - specifically,
it may very reasonably be "bolt+routing://path-to-some-load-balancer".
  • Loading branch information
jakewins authored and lutovich committed Jun 20, 2018
1 parent 0c75e24 commit 286bf1b
Show file tree
Hide file tree
Showing 20 changed files with 542 additions and 134 deletions.
Expand Up @@ -19,6 +19,11 @@
*/
package org.neo4j.server.plugins;

/**
* Used to allow custom values to be injected into JAX-RS classes.
*
* @param <T> the type of the value, or an interface the value implements.
*/
public interface Injectable<T>
{
/**
Expand All @@ -28,6 +33,38 @@ public interface Injectable<T>
*/
T getValue();

/**
* The type that resources should ask for to get this value;
* this can either be the concrete class, or some interface the
* value instance implements.
*
* @return a class that methods that want this value injected should ask for
*/
Class<T> getType();

/**
* Utility to wrap a singleton value as an injectable.
*
* @param type the type that JAX-RS classes should ask for
* @param obj the value
* @param <T> same as type
* @return
*/
static <T> Injectable<T> injectable( Class<T> type, T obj )
{
return new Injectable<T>()
{
@Override
public T getValue()
{
return obj;
}

@Override
public Class<T> getType()
{
return type;
}
};
}
}
Expand Up @@ -31,11 +31,20 @@ public class MappingSerializer extends Serializer
this.writer = writer;
}

/**
* @deprecated please use {@link #putAbsoluteUri(String, URI)}
*/
@Deprecated
void putAbsoluteUri( String key, String path )
{
writer.writeValue( RepresentationType.URI, key, path );
}

void putAbsoluteUri( String key, URI path )
{
writer.writeValue( RepresentationType.URI, key, path.toASCIIString() );
}

public void putRelativeUri( String key, String path )
{
writer.writeValue( RepresentationType.URI, key, relativeUri( path ) );
Expand Down
Expand Up @@ -138,8 +138,7 @@ public abstract class AbstractNeoServer implements NeoServer

private TransactionHandleRegistry transactionRegistry;
private boolean initialized;
private LifecycleAdapter serverComponents;
protected ConnectorPortRegister connectorPortRegister;
private ConnectorPortRegister connectorPortRegister;
private HttpConnector httpConnector;
private Optional<HttpConnector> httpsConnector;
private AsyncRequestLog requestLog;
Expand Down Expand Up @@ -196,8 +195,7 @@ public void init()
registerModule( moduleClass );
}

serverComponents = new ServerComponentsLifecycleAdapter();
life.add( serverComponents );
life.add( new ServerComponentsLifecycleAdapter() );

this.initialized = true;
}
Expand All @@ -209,7 +207,6 @@ public void start() throws ServerStartupException
try
{
life.start();

}
catch ( Throwable t )
{
Expand Down Expand Up @@ -469,7 +466,6 @@ protected Collection<InjectableProvider<?>> createDefaultInjectables()
singletons.add( new TransactionFilter( database ) );
singletons.add( new LoggingProvider( logProvider ) );
singletons.add( providerForSingleton( logProvider.getLog( NeoServer.class ), Log.class ) );

singletons.add( providerForSingleton( resolveDependency( UsageData.class ), UsageData.class ) );

return singletons;
Expand Down
Expand Up @@ -23,11 +23,13 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.Supplier;

import org.neo4j.graphdb.facade.GraphDatabaseFacadeFactory;
import org.neo4j.graphdb.factory.GraphDatabaseSettings;
import org.neo4j.graphdb.factory.module.CommunityEditionModule;
import org.neo4j.kernel.configuration.Config;
import org.neo4j.kernel.configuration.ConnectorPortRegister;
import org.neo4j.kernel.impl.factory.DatabaseInfo;
import org.neo4j.logging.LogProvider;
import org.neo4j.server.database.Database;
Expand All @@ -41,13 +43,15 @@
import org.neo4j.server.modules.SecurityRulesModule;
import org.neo4j.server.modules.ServerModule;
import org.neo4j.server.modules.ThirdPartyJAXRSModule;
import org.neo4j.server.rest.discovery.DiscoverableURIs;
import org.neo4j.server.rest.management.AdvertisableService;
import org.neo4j.server.rest.management.JmxService;
import org.neo4j.server.rest.management.console.ConsoleService;
import org.neo4j.server.web.Jetty9WebServer;
import org.neo4j.server.web.WebServer;

import static org.neo4j.server.database.LifecycleManagingDatabase.lifecycleManagingDatabase;
import static org.neo4j.server.rest.discovery.CommunityDiscoverableURIs.communityDiscoverableURIs;

public class CommunityNeoServer extends AbstractNeoServer
{
Expand All @@ -74,7 +78,7 @@ public CommunityNeoServer( Config config, Database.Factory dbFactory, GraphDatab
protected Iterable<ServerModule> createServerModules()
{
return Arrays.asList(
new DBMSModule( webServer, getConfig() ),
createDBMSModule(),
new RESTApiModule( webServer, getConfig(), getDependencyResolver(), logProvider ),
new ManagementApiModule( webServer, getConfig() ),
new ThirdPartyJAXRSModule( webServer, getConfig(), logProvider, this ),
Expand All @@ -100,6 +104,14 @@ public Iterable<AdvertisableService> getServices()
return toReturn;
}

protected DBMSModule createDBMSModule()
{
// ConnectorPortRegister isn't available until runtime, so defer loading until then
Supplier<DiscoverableURIs> discoverableURIs = () -> communityDiscoverableURIs( getConfig(),
getDependencyResolver().resolveDependency( ConnectorPortRegister.class ) );
return new DBMSModule( webServer, getConfig(), discoverableURIs );
}

protected AuthorizationModule createAuthorizationModule()
{
return new AuthorizationModule( webServer, authManagerSupplier, logProvider, getConfig(), getUriWhitelist() );
Expand Down
Expand Up @@ -192,6 +192,15 @@ private ThirdPartyJaxRsPackage createThirdPartyJaxRsPackage( String packageAndMo
"Value is expected to contain dirictives like 'max-age', 'includeSubDomains' and 'preload'." )
public static final Setting<String> http_strict_transport_security = setting( "dbms.security.http_strict_transport_security", STRING, NO_DEFAULT );

@Internal
@Description( "Publicly discoverable bolt:// URI to use for Neo4j Drivers wanting to access the data in this " +
"particular database instance. Normally this is the same as the advertised address configured for the " +
"connector, but this allows manually overriding that default." )
@DocumentedDefaultValue(
"Defaults to a bolt://-schemed version of the advertised address " + "of the first found bolt connector." )
public static final Setting<URI> bolt_discoverable_address =
setting( "unsupported.dbms.discoverable_bolt_address", Settings.URI, "" );

@SuppressWarnings( "unused" ) // accessed from the browser
@Description( "Commands to be run when Neo4j Browser successfully connects to this server. Separate multiple " +
"commands with semi-colon." )
Expand Down
Expand Up @@ -21,13 +21,18 @@

import java.util.ArrayList;
import java.util.List;
import java.util.function.Supplier;

import org.neo4j.graphdb.factory.GraphDatabaseSettings;
import org.neo4j.kernel.configuration.Config;
import org.neo4j.server.rest.dbms.UserService;
import org.neo4j.server.rest.discovery.DiscoverableURIs;
import org.neo4j.server.rest.discovery.DiscoveryService;
import org.neo4j.server.web.WebServer;

import static java.util.Collections.singletonList;
import static org.neo4j.server.plugins.Injectable.injectable;

/**
* Mounts the DBMS REST API.
*/
Expand All @@ -37,24 +42,29 @@ public class DBMSModule implements ServerModule

private final WebServer webServer;
private final Config config;
private final Supplier<DiscoverableURIs> discoverableURIs;

public DBMSModule( WebServer webServer, Config config )
public DBMSModule( WebServer webServer, Config config, Supplier<DiscoverableURIs> discoverableURIs )
{
this.webServer = webServer;
this.config = config;
this.discoverableURIs = discoverableURIs;
}

@Override
public void start()
{
webServer.addJAXRSClasses(
singletonList( DiscoveryService.class.getName() ), ROOT_PATH,
singletonList( injectable( DiscoverableURIs.class, discoverableURIs.get() ) ) );
webServer.addJAXRSClasses( getClassNames(), ROOT_PATH, null );

}

private List<String> getClassNames()
{
List<String> toReturn = new ArrayList<>( 2 );

toReturn.add( DiscoveryService.class.getName() );
if ( config.get( GraphDatabaseSettings.auth_enabled ) )
{
toReturn.add( UserService.class.getName() );
Expand Down
@@ -0,0 +1,42 @@
/*
* Copyright (c) 2002-2018 "Neo4j,"
* Neo4j Sweden AB [http://neo4j.com]
*
* This file is part of Neo4j.
*
* Neo4j is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.neo4j.server.rest.discovery;

import org.neo4j.kernel.configuration.Config;
import org.neo4j.kernel.configuration.ConnectorPortRegister;
import org.neo4j.server.configuration.ServerSettings;

public class CommunityDiscoverableURIs
{
/**
* URIs exposed at the root HTTP endpoint, to help clients discover the rest of the service.
*/
public static DiscoverableURIs communityDiscoverableURIs( Config config, ConnectorPortRegister portRegister )
{
DiscoverableURIs repo = new DiscoverableURIs();
repo.addRelative( "data", config.get( ServerSettings.rest_api_path ).getPath() + "/" );
repo.addRelative( "management", config.get( ServerSettings.management_api_path ).getPath() + "/" );

DiscoverableURIs
.discoverableBoltUri("bolt", config, ServerSettings.bolt_discoverable_address, portRegister )
.ifPresent( uri -> repo.addAbsolute( "bolt", uri ) );
return repo;
}
}
@@ -0,0 +1,107 @@
/*
* Copyright (c) 2002-2018 "Neo4j,"
* Neo4j Sweden AB [http://neo4j.com]
*
* This file is part of Neo4j.
*
* Neo4j is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.neo4j.server.rest.discovery;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Optional;
import java.util.function.BiConsumer;

import org.neo4j.graphdb.config.InvalidSettingException;
import org.neo4j.graphdb.config.Setting;
import org.neo4j.helpers.AdvertisedSocketAddress;
import org.neo4j.helpers.collection.Pair;
import org.neo4j.kernel.configuration.Config;
import org.neo4j.kernel.configuration.ConnectorPortRegister;

/**
* Repository of URIs that the REST API publicly advertises at the root endpoint.
*/
public class DiscoverableURIs
{
private final Collection<Pair<String,String>> relativeUris = new ArrayList<>();
private final Collection<Pair<String,URI>> absoluteUris = new ArrayList<>();

public DiscoverableURIs addRelative( String key, String uri )
{
relativeUris.add( Pair.pair( key, uri ) );
return this;
}

public DiscoverableURIs addAbsolute( String key, URI uri )
{
absoluteUris.add( Pair.pair( key, uri ) );
return this;
}

public static Optional<URI> discoverableBoltUri( String scheme, Config config, Setting<URI> override,
ConnectorPortRegister connectorPortRegister )
{
// Note that this whole function would be much cleaner to implement as a default function for the
// bolt_discoverable_address setting; however the current config design makes it hard to do
// "find any bolt connector", it can only do "find exactly this config key".. Something to refactor
// when we refactor config API for 4.0.
if ( config.isConfigured( override ) )
{
return Optional.ofNullable( config.get( override ) );
}

return config.enabledBoltConnectors().stream().findFirst().map( c ->
{
AdvertisedSocketAddress advertisedSocketAddress = config.get( c.advertised_address );

// If port is 0 it's been assigned a random port from the OS, list this instead
if ( advertisedSocketAddress.getPort() == 0 )
{
int boltPort = connectorPortRegister.getLocalAddress( c.key() ).getPort();
return boltURI( scheme, advertisedSocketAddress.getHostname(), boltPort );
}

// Use the config verbatim since it seems sane
return boltURI( scheme, advertisedSocketAddress.getHostname(), advertisedSocketAddress.getPort() );
} );
}

public void forEachRelativeUri( BiConsumer<String,String> consumer )
{
relativeUris.forEach( p -> consumer.accept( p.first(), p.other() ) );
}

public void forEachAbsoluteUri( BiConsumer<String,URI> consumer )
{
absoluteUris.forEach( p -> consumer.accept( p.first(), p.other() ) );
}

private static URI boltURI( String scheme, String host, int port )
{
try
{
return new URI( scheme, null, host, port, null, null, null );
}
catch ( URISyntaxException e )
{
throw new InvalidSettingException(
String.format( "Unable to construct bolt discoverable URI using '%s' as hostname: " + "%s", host,
e.getMessage() ), e );
}
}
}

0 comments on commit 286bf1b

Please sign in to comment.