Server skeleton for Ruby

Prerequisites

Gems required for the server implementation:

  • google-protobuf (3.2.X used in this tutorial)
  • grpc (1.2.X used in this tutorial)

Download the service definition and create this directory structure:

[base_dir]
├── certificates
├── lib
├── protos
    └── booking_service.proto
└── server.rb

Generate Ruby libraries from the interface description:

$ cd [base_dir]
$ grpc_tools_ruby_protoc -I protos --ruby_out=lib --grpc_out=lib protos/booking_service.proto

Implement the server

Skeleton implementation:

#!/usr/bin/ruby2.0

# Copyright 2017, Google Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
#     * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#     * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following disclaimer
# in the documentation and/or other materials provided with the
# distribution.
#     * Neither the name of Google Inc. nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

# Sample gRPC server that implements the BookingService and enforces mutual
# authentication.
#
# Usage: $ path/to/server.rb

this_dir = File.expand_path(File.dirname(__FILE__))
lib_dir = File.join(this_dir, 'lib')
$LOAD_PATH.unshift(lib_dir) unless $LOAD_PATH.include?(lib_dir)

require 'grpc'
require 'grpc/health/checker'
require 'grpc/health/v1/health_services_pb'
require 'openssl'
require 'optparse'
require 'booking_service_services_pb.rb'

# Peer certificate validation that checks for a specific CN in the subject
# name. Add additional valid CNs, e.g. for a test client, to the array.
ACCEPTED_CNS = ["mapsbooking.businesslink-3.net"]
def check_peer_cert(grpc_call)
  valid_cert = false
  certificate = OpenSSL::X509::Certificate.new grpc_call.peer_cert
  certificate.subject().to_a().each do |name_entry|
    if (name_entry[0] == "CN") && ACCEPTED_CNS.include?(name_entry[1])
      valid_cert = true
    end
  end
  unless valid_cert
    fail GRPC::BadStatus.new(GRPC::Core::StatusCodes::UNAUTHENTICATED,
                             "Client cert has invalid CN")
  end
end


# BookingServer is a simple server that implements the BookingService.
class BookingServer < Ext::Maps::Booking::Partner::V0::BookingService::Service

  PartnerApi = Ext::Maps::Booking::Partner::V0

  def initialize(peer_cert_validator)
    @peer_cert_validator = peer_cert_validator
  end

  def create_lease(lease_req, grpc_call)
    @peer_cert_validator.call(grpc_call)
    lease = lease_req.lease.dup
    # Perform availability check etc.
    #
    # For error conditions (example):
    #  fail GRPC::BadStatus.new(GRPC::Core::StatusCodes::RESOURCE_EXHAUSTED,
    #                           "Slot unavailable")
    #
    # Happy path: populate the response
    # [...]
    # Assign a lease ID
    # lease.lease_id = ...
    PartnerApi::CreateLeaseResponse.new(lease: lease)
  end

  def create_booking(booking_req, grpc_call)
    @peer_cert_validator.call(grpc_call)
    booking = PartnerApi::Booking.new
    # Populate booking
    # [...]
    # Assign a booking ID
    # booking.booking_id = ...
    PartnerApi::CreateBookingResponse.new(booking: booking)
  end

  def update_booking(booking_req, grpc_call)
    @peer_cert_validator.call(grpc_call)
    booking = PartnerApi::Booking.new
    # * Look up the booking with ID booking_req.booking.booking_id
    # * Update according to the request and return the updated booking.
    PartnerApi::UpdateBookingResponse.new(booking: booking)
  end

end


# Loads the certificates for the test server.
def load_server_certs
  this_dir = File.expand_path(File.dirname(__FILE__))
  cert_dir = File.join(this_dir, 'certificates')
  # In order:
  # * PEM file with trusted root certificates for client auth
  # * Server private key
  # * PEM file with server certificate chain
  files = ['trusted_client_roots.pem', 'server.key', 'server.pem']
  files.map { |f| File.open(File.join(cert_dir, f)).read }
end


# Creates ServerCredentials from certificates.
def server_credentials
  certs = load_server_certs
  GRPC::Core::ServerCredentials.new(
      certs[0], [{private_key: certs[1], cert_chain: certs[2]}], true)
end


def main
  # Parse command line arguments
  disable_tls = false
  OptionParser.new do |opts|
    opts.on('--disable_tls',
            'true to disable TLS. NOT FOR PRODUCTION USE!') do |v|
      disable_tls = v
    end
  end.parse!

  s = GRPC::RpcServer.new
  # Listen on port 50051 on all interfaces. Update for production use.
  s.add_http2_port('[::]:50051',
                   disable_tls ? :this_port_is_insecure : server_credentials)

  cert_validator = disable_tls ? ->(grpc_call) {} : method(:check_peer_cert)
  s.handle(BookingServer.new cert_validator)

  health_checker = Grpc::Health::Checker.new
  health_checker.add_status(
      "ext.maps.booking.partner.v0.BookingService",
      Grpc::Health::V1::HealthCheckResponse::ServingStatus::SERVING)
  s.handle(health_checker)
  s.run_till_terminated
end

if __FILE__ == $0
  main()
end

Test the server without TLS

For initial testing, TLS can be disabled:

$ cd [base_dir]
$ ruby server.rb --disable_tls

This is unsuitable for production use!

Configure production certificates

To enable TLS on the server, the following files are required:

  • certificates/server.pem the certificate chain for the server in in PEM format
  • certificates/server.key the private key for the server certificate chain
  • certificates/trusted_client_roots.pem the root certificates that are trusted when authenticating clients

The set of trusted client root certificates is used when authenticating the client. You can choose to obtain this set of trusted roots from an authority like Mozilla or install the set of roots currently recommended by the Google Internet Authority G2. In the latter case, you may have to manually update the root certificate at times.

Final directory structure

[base_dir]
├── certificates
    ├── server.pem
    ├── server.key
    └── trusted_client_roots.pem
├── lib
    ├── booking_service_pb.rb
    └── booking_service_services_pb.rb
├── protos
    └── booking_service.proto
└── server.rb