Skip to content

Commit

Permalink
SSL echo client and server example
Browse files Browse the repository at this point in the history
Summary:
Example code for:
- Server using wangle::Acceptor that can speak TLS and plaintext on the same port and refreshes credentials on change.
- Client that uses the SSL session cache

Reviewed By: anirudhvr

Differential Revision: D5720113

fbshipit-source-id: e773aee43726179f14a9fede7e59a93c7825f856
  • Loading branch information
ngoyal authored and facebook-github-bot committed Mar 5, 2018
1 parent 21e9e7d commit bd24ccc
Show file tree
Hide file tree
Showing 2 changed files with 348 additions and 0 deletions.
178 changes: 178 additions & 0 deletions wangle/example/ssl/Client.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
/*
* Copyright 2018-present Facebook, 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
*
* https://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.
*/
#include <iostream>
#include <folly/init/Init.h>
#include <folly/io/async/AsyncSSLSocket.h>
#include <folly/io/async/SSLOptions.h>
#include <folly/portability/GFlags.h>
#include <folly/ssl/Init.h>
#include <folly/io/async/DelayedDestruction.h>

#include <wangle/bootstrap/ClientBootstrap.h>
#include <wangle/channel/AsyncSocketHandler.h>
#include <wangle/channel/EventBaseHandler.h>
#include <wangle/client/ssl/SSLSessionPersistentCache.h>
#include <wangle/client/persistence/LRUPersistentCache.h>
#include <wangle/codec/LineBasedFrameDecoder.h>
#include <wangle/codec/StringCodec.h>

using namespace wangle;
using namespace folly;

DEFINE_string(ip, "::1", "Ip address to connect to");
DEFINE_uint32(port, 8080, "Port to connect to");
DEFINE_bool(ssl, true, "Whether to use SSL");
DEFINE_string(cache_file, "", "Session cache file");
DEFINE_string(cert_path, "", "Path to client cert pem");
DEFINE_string(key_path, "", "Path to client cert key pem");
DEFINE_string(ca_path, "", "Path to trusted CA file");

const std::string SVC_IDENTITY = "test_client";

/**
* This is meant to be a simple client that can be configured for SSL or
* plaintext. To test:
* openssl s_server -key <key_path> -cert <cert_path> -accept <port>
* OR: ./server --cert_path <cert_path> --key_path <key_path>
*
* Run ./client and enter text
*
* Test cert and key are available in wangle/ssl/test/certs
*/
namespace {
typedef Pipeline<folly::IOBufQueue&, std::string> EchoPipeline;

// the handler for receiving messages back from the server
class EchoHandler : public HandlerAdapter<std::string> {
public:
void read(Context*, std::string msg) override {
std::cout << "received back: " << msg;
}
void readException(Context* ctx, exception_wrapper e) override {
std::cout << exceptionStr(e) << std::endl;
close(ctx);
}
void readEOF(Context* ctx) override {
std::cout << "EOF received :(" << std::endl;
close(ctx);
}
};

// chains the handlers together to define the response pipeline
class EchoPipelineFactory : public PipelineFactory<EchoPipeline> {
public:
EchoPipeline::Ptr newPipeline(
std::shared_ptr<AsyncTransportWrapper> sock) override {
auto pipeline = EchoPipeline::create();
pipeline->addBack(AsyncSocketHandler(sock));
pipeline->addBack(
EventBaseHandler()); // ensure we can write from any thread
pipeline->addBack(LineBasedFrameDecoder(8192, false));
pipeline->addBack(StringCodec());
pipeline->addBack(EchoHandler());
pipeline->finalize();
return pipeline;
}
};

class EchoClientBootstrap : public ClientBootstrap<EchoPipeline> {
public:
virtual void makePipeline(std::shared_ptr<folly::AsyncSocket> socket) {
auto sslSock = socket->getUnderlyingTransport<AsyncSSLSocket>();
if (sslSock) {
// service identity ends up being the session cache key
sslSock->setServiceIdentity(SVC_IDENTITY);
}
ClientBootstrap<EchoPipeline>::makePipeline(std::move(socket));
}
};

std::shared_ptr<SSLContext> createSSLContext() {
auto context = std::make_shared<SSLContext>();
if (!FLAGS_ca_path.empty()) {
context->loadTrustedCertificates(FLAGS_ca_path.c_str());
// don't do peer name validation
context->authenticate(true, false);
// verify the server cert
context->setVerificationOption(SSLContext::SSLVerifyPeerEnum::VERIFY);
}
if (!FLAGS_cert_path.empty() && !FLAGS_key_path.empty()) {
context->loadCertificate(FLAGS_cert_path.c_str());
context->loadPrivateKey(FLAGS_key_path.c_str());
if (!context->isCertKeyPairValid()) {
throw std::runtime_error("Cert and key do not match");
}
}
folly::ssl::setSignatureAlgorithms<folly::ssl::SSLCommonOptions>(*context);
return context;
}
} // namespace

int main(int argc, char** argv) {
folly::init(&argc, &argv);
folly::ssl::init();

// an in memory ssl session cache used for caching sessions
std::shared_ptr<SSLSessionPersistentCache> cache;
if (!FLAGS_cache_file.empty()) {
cache = std::make_shared<SSLSessionPersistentCache>(
FLAGS_cache_file, 100, std::chrono::seconds(60));
}

EchoClientBootstrap client;
client.group(std::make_shared<IOThreadPoolExecutor>(1));
client.pipelineFactory(std::make_shared<EchoPipelineFactory>());

if (FLAGS_ssl) {
auto ctx = createSSLContext();
// attach the context to the cache
if (cache) {
// service identity acts as the session cache key
wangle::SSLSessionCallbacks::attachCallbacksToContext(
ctx->getSSLCtx(), cache.get());
auto session = cache->getSSLSession(SVC_IDENTITY);
if (session) {
VLOG(0) << "Reusing session";
client.sslSession(session.release());
}
}
client.sslContext(ctx);
}

SocketAddress addr(FLAGS_ip.c_str(), FLAGS_port);
VLOG(0) << "Connecting";
auto pipeline = client.connect(addr).get();
VLOG(0) << "Connected";
try {
while (true) {
std::string line;
std::getline(std::cin, line);
if (line == "") {
VLOG(0) << "End";
break;
}
VLOG(0) << "Sending " << line;
pipeline->write(line + "\r\n").get();
if (line == "bye") {
pipeline->close();
break;
}
}
} catch (const std::exception& e) {
std::cout << exceptionStr(e) << std::endl;
}
return 0;
}
170 changes: 170 additions & 0 deletions wangle/example/ssl/Server.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
/*
* Copyright 2018-present Facebook, 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
*
* https://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.
*/
#include <folly/init/Init.h>
#include <folly/io/async/AsyncSSLSocket.h>
#include <folly/portability/GFlags.h>
#include <folly/ssl/Init.h>
#include <wangle/acceptor/Acceptor.h>
#include <wangle/bootstrap/ServerBootstrap.h>
#include <wangle/channel/AsyncSocketHandler.h>
#include <wangle/codec/LineBasedFrameDecoder.h>
#include <wangle/codec/StringCodec.h>
#include <wangle/ssl/TLSCredProcessor.h>

DEFINE_string(cert_path, "", "Path to cert pem");
DEFINE_string(key_path, "", "Path to cert key");
DEFINE_string(ca_path, "", "Path to trusted CA file");
DEFINE_int32(port, 8080, "Listen port");
DEFINE_string(tickets_path, "", "Path for ticket seeds");
DEFINE_uint32(num_workers, 2, "Number of worker threads");

/**
* This is meant to be a simple server that accepts plaintext and SSL on a
* single port. To test:
* plaintext: nc localhost 8080
* ssl: openssl s_client -connect localhost:8080
*
* Test cert and key are available in wangle/ssl/test/certs
*/
using namespace wangle;
using namespace folly;

typedef Pipeline<IOBufQueue&, std::string> EchoPipeline;

namespace {
// the main logic of our echo server; receives a string and writes it straight
// back
class EchoHandler : public HandlerAdapter<std::string> {
public:
void read(Context* ctx, std::string msg) override {
std::cout << "handling " << msg << std::endl;
write(ctx, msg + "\r\n");
}
};

// where we define the chain of handlers for each messeage received
class EchoPipelineFactory : public PipelineFactory<EchoPipeline> {
public:
EchoPipeline::Ptr newPipeline(
std::shared_ptr<AsyncTransportWrapper> sock) override {
auto pipeline = EchoPipeline::create();
pipeline->addBack(AsyncSocketHandler(sock));
pipeline->addBack(LineBasedFrameDecoder(8192));
pipeline->addBack(StringCodec());
pipeline->addBack(EchoHandler());
pipeline->finalize();
return pipeline;
}
};

// Init the processor callbacks. It's fine to do this
// even if nothing is being watched
void initCredProcessorCallbacks(
ServerBootstrap<EchoPipeline>& sb,
TLSCredProcessor& processor) {
// set up ticket seed callback
processor.addTicketCallback([&](TLSTicketKeySeeds seeds) {
// update
sb.forEachWorker([&](Acceptor* acceptor) {
if (!acceptor) {
// this condition can happen if the processor triggers before the
// server is ready / listening
return;
}
auto evb = acceptor->getEventBase();
if (!evb) {
return;
}
evb->runInEventBaseThread([acceptor, seeds] {
acceptor->setTLSTicketSecrets(
seeds.oldSeeds, seeds.currentSeeds, seeds.newSeeds);
});
});
});

// Reconfigure SSL when we detect cert or CA changes.
processor.addCertCallback([&] {
sb.forEachWorker([&](Acceptor* acceptor) {
if (!acceptor) {
return;
}
auto evb = acceptor->getEventBase();
if (!evb) {
return;
}
evb->runInEventBaseThread([acceptor] {
acceptor->resetSSLContextConfigs();
});
});
});
}
} // namespace

int main(int argc, char** argv) {
folly::init(&argc, &argv);
folly::ssl::init();

ServerSocketConfig cfg;
folly::Optional<TLSTicketKeySeeds> seeds;

ServerBootstrap<EchoPipeline> sb;
TLSCredProcessor processor;

if (!FLAGS_tickets_path.empty()) {
seeds = TLSCredProcessor::processTLSTickets(FLAGS_tickets_path);
if (seeds) {
cfg.initialTicketSeeds = *seeds;
// watch for changes
processor.setTicketPathToWatch(FLAGS_tickets_path);
}
}

if (!FLAGS_cert_path.empty() && !FLAGS_key_path.empty()) {
VLOG(0) << "Configuring SSL";
SSLContextConfig sslCfg;
sslCfg.addCertificate(FLAGS_cert_path, FLAGS_key_path, "");
sslCfg.clientCAFile = FLAGS_ca_path;
sslCfg.isDefault = true;
cfg.sslContextConfigs.push_back(sslCfg);
// IMPORTANT: when allowing both plaintext and ssl on the same port,
// the acceptor requires 9 bytes of data to determine what kind of
// connection is coming in. If the client does not send 9 bytes the
// connection will idle out before the EchoCallback receives data.
cfg.allowInsecureConnectionsOnSecureServer = true;

// reload ssl contexts when certs change
std::set<std::string> pathsToWatch { FLAGS_cert_path, FLAGS_key_path };
if (!FLAGS_ca_path.empty()) {
pathsToWatch.insert(FLAGS_ca_path);
}
processor.setCertPathsToWatch(std::move(pathsToWatch));
}

initCredProcessorCallbacks(sb, processor);

// workers
auto workers =
std::make_shared<folly::IOThreadPoolExecutor>(FLAGS_num_workers);

// create a server
sb.acceptorConfig(cfg);
sb.childPipeline(std::make_shared<EchoPipelineFactory>());
sb.group(workers);

sb.bind(FLAGS_port);
sb.waitForStop();
return 0;
}

0 comments on commit bd24ccc

Please sign in to comment.