Ruby 版服务器框架

前提条件

服务器实现所需的 Gem:

  • google-protobuf(本教程中使用的 3.2.X 版)
  • grpc(本教程中使用了 1.2.X)

下载服务定义并创建以下目录结构:

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

根据接口说明生成 Ruby 库:

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

实现服务器

框架实现:

#!/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

在不使用 TLS 的情况下测试服务器

对于初始测试,可以停用 TLS:

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

这不适合用于生产用途!

配置生产证书

如需在服务器上启用 TLS,需要以下文件:

  • certificates/server.pem:服务器的证书链(采用 PEM 格式)
  • certificates/server.key:服务器证书链的私钥
  • certificates/trusted_client_roots.pem:对客户端进行身份验证时信任的根证书

对客户端进行身份验证时,将使用受信任的客户端根证书集。您可以选择从 Mozilla 等机构获取这组可信根,也可以安装 Google 互联网管理局 G2 当前建议的这组可信根。在后一种情况下,您可能必须不时手动更新根证书。

最终目录结构

[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