/** * Copyright 2016 Yahoo Inc. * * Licensed 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 com.yahoo.pulsar.discovery.service; import static com.google.common.base.Preconditions.checkArgument; import static com.yahoo.pulsar.common.api.proto.PulsarApi.CommandLookupTopicResponse.LookupType.Redirect; import java.util.concurrent.TimeUnit; import javax.naming.AuthenticationException; import javax.net.ssl.SSLSession; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.yahoo.pulsar.broker.authentication.AuthenticationDataCommand; import com.yahoo.pulsar.common.api.Commands; import com.yahoo.pulsar.common.api.PulsarHandler; import com.yahoo.pulsar.common.api.proto.PulsarApi.CommandConnect; import com.yahoo.pulsar.common.api.proto.PulsarApi.CommandLookupTopic; import com.yahoo.pulsar.common.api.proto.PulsarApi.CommandPartitionedTopicMetadata; import com.yahoo.pulsar.common.api.proto.PulsarApi.ServerError; import com.yahoo.pulsar.common.naming.DestinationName; import com.yahoo.pulsar.common.policies.data.loadbalancer.LoadReport; import io.netty.channel.ChannelHandler; import io.netty.handler.ssl.SslHandler; /** * Handles incoming discovery request from client and sends appropriate response back to client * */ public class ServerConnection extends PulsarHandler { private DiscoveryService service; private String authRole = null; private State state; public static final String TLS_HANDLER = "tls"; enum State { Start, Connected } public ServerConnection(DiscoveryService discoveryService) { super(0, TimeUnit.SECONDS); // discovery-service doesn't need to run keepAlive task this.service = discoveryService; this.state = State.Start; } /** * handles connect request and sends {@code State.Connected} ack to client */ @Override protected void handleConnect(CommandConnect connect) { checkArgument(state == State.Start); if (LOG.isDebugEnabled()) { LOG.debug("Received CONNECT from {}", remoteAddress); } if(service.getConfiguration().isAuthenticationEnabled()) { try { String authMethod = "none"; if (connect.hasAuthMethodName()) { authMethod = connect.getAuthMethodName(); } else if (connect.hasAuthMethod()) { // Legacy client is passing enum authMethod = connect.getAuthMethod().name().substring(10).toLowerCase(); } String authData = connect.getAuthData().toStringUtf8(); ChannelHandler sslHandler = ctx.channel().pipeline().get(TLS_HANDLER); SSLSession sslSession = null; if (sslHandler != null) { sslSession = ((SslHandler) sslHandler).engine().getSession(); } authRole = service.getAuthenticationService() .authenticate(new AuthenticationDataCommand(authData, remoteAddress, sslSession), authMethod); LOG.info("[{}] Client successfully authenticated with {} role {}", remoteAddress, authMethod, authRole); } catch (AuthenticationException e) { String msg = "Unable to authenticate"; LOG.warn("[{}] {}: {}", remoteAddress, msg, e.getMessage()); ctx.writeAndFlush(Commands.newError(-1, ServerError.AuthenticationError, msg)); close(); return; } } ctx.writeAndFlush(Commands.newConnected(connect)); state = State.Connected; remoteEndpointProtocolVersion = connect.getProtocolVersion(); } @Override protected void handlePartitionMetadataRequest(CommandPartitionedTopicMetadata partitionMetadata) { checkArgument(state == State.Connected); if (LOG.isDebugEnabled()) { LOG.debug("Received PartitionMetadataLookup from {}", remoteAddress); } sendPartitionMetadataResponse(partitionMetadata); } /** * handles discovery request from client ands sends next active broker address */ @Override protected void handleLookup(CommandLookupTopic lookup) { checkArgument(state == State.Connected); if (LOG.isDebugEnabled()) { LOG.debug("Received Lookup from {}", remoteAddress); } sendLookupResponse(lookup.getRequestId()); } private void close() { ctx.close(); } private void sendLookupResponse(long requestId) { try { LoadReport availableBroker = service.getDiscoveryProvider().nextBroker(); ctx.writeAndFlush(Commands.newLookupResponse(availableBroker.getPulsarServiceUrl(), availableBroker.getPulsarServiceUrlTls(), false, Redirect, requestId)); } catch (PulsarServerException e) { LOG.warn("[{}] Failed to get next active broker {}", remoteAddress, e.getMessage(), e); ctx.writeAndFlush( Commands.newLookupResponse(ServerError.ServiceNotReady, e.getMessage(), requestId)); } } private void sendPartitionMetadataResponse(CommandPartitionedTopicMetadata partitionMetadata) { final long requestId = partitionMetadata.getRequestId(); DestinationName dn = DestinationName.get(partitionMetadata.getTopic()); service.getDiscoveryProvider().getPartitionedTopicMetadata(service, dn, authRole).thenAccept(metadata -> { if (LOG.isDebugEnabled()) { LOG.debug("[{}] Total number of partitions for topic {} is {}", authRole, dn, metadata.partitions); } ctx.writeAndFlush(Commands.newPartitionMetadataResponse(metadata.partitions, requestId)); }).exceptionally(ex -> { LOG.warn("[{}] Failed to get partitioned metadata for topic {} {}", remoteAddress, dn, ex.getMessage(), ex); ctx.writeAndFlush( Commands.newPartitionMetadataResponse(ServerError.ServiceNotReady, ex.getMessage(), requestId)); return null; }); } @Override protected boolean isHandshakeCompleted() { return state == State.Connected; } private static final Logger LOG = LoggerFactory.getLogger(ServerConnection.class); }