OpenC3/cosmos

View on GitHub
openc3/lib/openc3/interfaces/http_server_interface.rb

Summary

Maintainability
A
2 hrs
Test Coverage
# encoding: ascii-8bit

# Copyright 2024 OpenC3, Inc.
# All Rights Reserved.
#
# This program is free software; you can modify and/or redistribute it
# under the terms of the GNU Affero General Public License
# as published by the Free Software Foundation; version 3 with
# attribution addendums as found in the LICENSE.txt
#
# 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 Affero General Public License for more details.
#
# This file may also be used under the terms of a commercial license
# if purchased from OpenC3, Inc.

require 'openc3/interfaces/interface'
require 'openc3/config/config_parser'
require 'openc3/accessors/http_accessor'
require 'webrick'

module OpenC3
  class HttpServerInterface < Interface
    # @param port [Integer] HTTP port
    def initialize(port = 80)
      super()
      @listen_address = '0.0.0.0' # Default to ANY
      @port = Integer(port)
      @server = nil
      @request_queue = Queue.new
    end

    # Supported Options
    # LISTEN_ADDRESS - Ip address of the interface to accept connections on
    # (see Interface#set_option)
    def set_option(option_name, option_values)
      super(option_name, option_values)
      if option_name.upcase == 'LISTEN_ADDRESS'
        @listen_address = option_values[0]
      end
    end

    def connection_string
      return "listening on #{@listen_address}:#{@port}"
    end

    # Connects the interface to its target(s)
    def connect
      @server = WEBrick::HTTPServer.new(:BindAddress => @listen_address, :Port => @port)
      @request_queue = Queue.new

      # Create a response hook for every command packet
      @target_names.each do |target_name|
        System.commands.packets(target_name).each do |packet_name, packet|
          packet.restore_defaults
          path = nil
          begin
            path = packet.read('HTTP_PATH')
          rescue => e
            # No HTTP_PATH is an error
            Logger.error("HttpServerInterface Packet #{target_name} #{packet_name} unable to read HTTP_PATH\n#{e.formatted}")
          end
          if path
            @server.mount_proc path do |req, res|
              # Build the Response
              begin
                status = packet.read('HTTP_STATUS')
                if status
                  res.status = status
                end
              rescue
                # No HTTP_STATUS - Leave at default
              end

              # http_accessor stores all the pseudo-derived HTTP configuration in extra
              if packet.extra
                headers = packet.extra['HTTP_HEADERS']
                if headers
                  headers.each do |key, value|
                    res[key] = value
                  end
                end
              end

              res.body = packet.buffer

              # Save the Request
              packet_name = nil
              begin
                packet_name = packet.read('HTTP_PACKET')
              rescue
                # No packet name means dont save the request as telemetry
              end
              if packet_name
                data = req.body.to_s.dup # Dup to remove frozen
                extra = {}
                extra['HTTP_REQUEST_TARGET_NAME'] = target_name
                extra['HTTP_REQUEST_PACKET_NAME'] = packet_name

                headers = req.header
                if headers
                  extra['HTTP_HEADERS'] = {}
                  headers.each do |key, value|
                    extra['HTTP_HEADERS'][key.downcase] = value
                  end
                end

                queries = req.query
                if queries
                  extra['HTTP_QUERIES'] = {}
                  queries.each do |key, value|
                    extra['HTTP_QUERIES'][key] = value
                  end
                end

                @request_queue << [data, extra]
              end
            end
          end
        end
      end

      super()

      Thread.new do
        # This blocks, but will be unblocked by server.shutdown called in disconnect()
        @server.start
      end
    end

    def connected?
      if @server
        return true
      else
        return false
      end
    end

    # Disconnects the interface from its target(s)
    def disconnect
      @server.shutdown if @server
      @server = nil
      while @request_queue.length > 0
        @request_queue.pop
      end
      super()
      @request_queue.push(nil)
    end

    # Reads from the socket if the read_port is defined
    def read_interface
      # Get the Faraday Response
      data, extra = @request_queue.pop
      return nil if data.nil?

      read_interface_base(data, extra)
      return data, extra
    end

    # Writes to the socket
    # @param data [Hash] For the HTTP Interface, data is a hash with the needed request info
    def write_interface(_data, _extra = nil)
      raise "Commands cannot be sent to HttpServerInterface"
    end

    # Called to convert the read data into a OpenC3 Packet object
    #
    # @param data [String] Raw packet data
    # @return [Packet] OpenC3 Packet with buffer filled with data
    def convert_data_to_packet(data, extra = nil)
      packet = Packet.new(nil, nil, :BIG_ENDIAN, nil, data.to_s)
      packet.accessor = HttpAccessor.new(packet)
      if extra
        # Identify the response
        request_target_name = extra['HTTP_REQUEST_TARGET_NAME']
        request_packet_name = extra['HTTP_REQUEST_PACKET_NAME']
        if request_target_name and request_packet_name
          packet.target_name = request_target_name.to_s.upcase
          packet.packet_name = request_packet_name.to_s.upcase
        end
        extra.delete("HTTP_REQUEST_TARGET_NAME")
        extra.delete("HTTP_REQUEST_PACKET_NAME")
        packet.extra = extra
      end

      return packet
    end

    # Called to convert a packet into the data to send
    #
    # @param packet [Packet] Packet to extract data from
    # @return data
    def convert_packet_to_data(_packet)
      raise "Commands cannot be sent to HttpServerInterface"
    end
  end
end