rastating/wordpress-exploit-framework

View on GitHub
lib/wpxf/net/http_server.rb

Summary

Maintainability
A
45 mins
Test Coverage
# frozen_string_literal: true

require 'cgi'
require 'socket'

# Provides basic, single threaded HTTP server functionality.
module Wpxf::Net::HttpServer
  include Wpxf

  # Initialize a new instance of {HttpServer}.
  def initialize
    super

    register_options([
      StringOption.new(
        name: 'http_server_bind_address',
        desc: 'Address to bind the HTTP server to',
        default: '0.0.0.0',
        required: true
      ),
      PortOption.new(
        name: 'http_server_bind_port',
        desc: 'Port for the HTTP server to listen on',
        default: 80,
        required: true
      )
    ])

    @http_server_kill_switch = false
  end

  # @return [String] the address the HTTP server is bound to.
  def http_server_bind_address
    normalized_option_value('http_server_bind_address')
  end

  # @return [Integer] the port the HTTP server is listening on.
  def http_server_bind_port
    normalized_option_value('http_server_bind_port')
  end

  # Invoked when a HTTP request is made to the server.
  # @param path [String] the path requested.
  # @param params [Hash] the query string parameters.
  # @param headers [Hash] the HTTP headers.
  # @return [String, Hash] if a string is returned, it will be used as the response body
  #  to send to the client. If a hash is returned, it should contain the keys:
  #  * +:body+ - the body text of the response.
  #  * +:type+ - the MIME type of the response.
  #  * +:headers+ - an array of header strings.
  def on_http_request(path, params, headers) end

  # @return [String] the AJAX download helper script.
  def js_ajax_download
    File.read(File.join(Wpxf.data_directory, 'js', 'ajax_download.js'))
  end

  # @return [String] the AJAX post helper script.
  def js_ajax_post
    File.read(File.join(Wpxf.data_directory, 'js', 'ajax_post.js'))
  end

  # @return [String] the JS post helper script.
  def js_post
    File.read(File.join(Wpxf.data_directory, 'js', 'post.js'))
  end

  # Start the HTTP server.
  # @param non_block [Boolean] run the server in non-blocking mode.
  def start_http_server(non_block = false)
    @tcp_server = TCPServer.new(http_server_bind_address, http_server_bind_port)
    emit_info "Started HTTP server on #{http_server_bind_address}:"\
              "#{http_server_bind_port}"

    if non_block
      @http_server_thread = Thread.new do
        _http_server_loop
      end
    else
      _http_server_loop
    end
  end

  # Stop the HTTP server after it has finished processing the current request.
  def stop_http_server
    return false if @is_stopping
    @is_stopping = true

    emit_info 'Stopping HTTP server...', true
    @http_server_kill_switch = true
    @http_server_thread&.exit
    @tcp_server.close if !@tcp_server.nil? && !@tcp_server.closed?
    emit_info 'HTTP server stopped'
    @is_stopping = false
  end

  # @return [Thread] thread that the server runs on when in non-blocking mode.
  def http_server_thread
    @http_server_thread
  end

  private

  def _send_headers(socket, body, content_type, custom_headers)
    headers = []
    headers.push 'HTTP/1.1 200 OK'
    headers.push "Content-Type: #{content_type}"
    headers.push "Content-Length: #{body.bytesize}"
    headers += custom_headers unless custom_headers.nil?
    headers.push 'Connection: close'

    headers.each do |header|
      socket.print "#{header}\r\n"
    end
  end

  def _send_response(response, socket)
    content_type = 'text/plain'
    body = ''
    custom_headers = nil

    if response.is_a? String
      body = response
    else
      body = response[:body]
      content_type = response[:type]
      custom_headers = response[:headers]
    end

    _send_headers(socket, body, content_type, custom_headers)
    socket.print "\r\n"
    socket.print body
  end

  def _http_server_loop
    begin
      loop do
        socket = @tcp_server.accept

        begin
          response = _handle_incoming_http_request(socket)
          _send_response(response, socket) if response
          socket.close
        rescue Errno::EPIPE
          emit_warning 'A socket was closed by the requester', true
        end

        break if @http_server_kill_switch
      end
    rescue SignalException
      emit_warning 'Caught kill signal', true
    rescue StandardError => e
      emit_error "Socket error: #{e}"
    end

    stop_http_server
  end

  def _handle_incoming_http_request(socket)
    request = socket.gets
    return unless request

    emit_info "Incoming HTTP request: #{request}", true

    headers = ''
    while (line = socket.gets) != "\r\n"
      headers += line
      emit_info line, true
    end

    headers = Hash[headers.each_line.map { |l| l.chomp.split(': ', 2) }]
    path = request.gsub(/^[A-Za-z]+\s(.+?)\s.*$/, '\1').chomp
    params = {}

    if path.include?('?')
      params = CGI.parse(path.split('?')[-1])
      params.each do |k, v|
        params[k] = v.join(' ')
      end
      path = path.split('?')[0]
    end

    # Dispatch parsed data to the callback in the module.
    on_http_request(path, params, headers)
  end
end