~pixelinc/gmod-checker.cr

c28f100d028ebc8d3072eab9c6ba830d33d8ec13 — PixeL 2 months ago 443b7d7
Move to HTTP::Server from Raze
3 files changed, 147 insertions(+), 102 deletions(-)

A src/handlers/socket_handler.cr
R src/{web.cr => handlers/web_router.cr}
M src/run.cr
A src/handlers/socket_handler.cr => src/handlers/socket_handler.cr +54 -0
@@ 0,0 1,54 @@
class Handlers::SocketHandler
  include HTTP::Handler

  def initialize
    @ws_handler = HTTP::WebSocketHandler.new(&->ws(HTTP::WebSocket, HTTP::Server::Context))
  end

  def serialize(nonce, result)
    JSON.build do |builder|
      builder.object do
        builder.field("nonce", nonce.to_s)
        builder.string "type"
        if result.is_a?(Job::PlayerResult)
          builder.string "player_result"
          builder.string "data"
          result.to_json(builder)
        elsif result.is_a?(Job::BatchPlayers)
          builder.string "batch_players"
          builder.string "data"
          result.to_json(builder)
        elsif result.is_a?(Job::Error)
          builder.string "error"
          builder.field("message", result.message)
          builder.field("data", result.id)
        elsif result.is_a?(Exception)
          builder.string "error"
          builder.field("message", result.message)
        end
      end
    end
  end


  def ws(socket : HTTP::WebSocket, context : HTTP::Server::Context)
    socket.on_message do |message|
      nonce = JobController::Nonce.from_json(message, "nonce")
      JobController.dispatch(nonce) do |result|
        payload = serialize(nonce, result)
        socket.send(payload)
      end
    rescue ex : JSON::ParseException | KeyError
      payload = serialize(nonce, ex)
      socket.send(payload)
    end
  end

  def call(context : HTTP::Server::Context)
    case context.request.path
    when "/api/relay" then @ws_handler.call(context)
    else
      call_next(context)
    end
  end
end
\ No newline at end of file

R src/web.cr => src/handlers/web_router.cr +71 -100
@@ 1,114 1,85 @@
require "raze"
require "kilt/slang"
require "http/client"
require "logger"
require "./steam"
require "./mappings"
require "./job_controller"

def serialize(nonce, result)
  JSON.build do |builder|
    builder.object do
      builder.field("nonce", nonce.to_s)
      builder.string "type"
      if result.is_a?(Job::PlayerResult)
        builder.string "player_result"
        builder.string "data"
        result.to_json(builder)
      elsif result.is_a?(Job::BatchPlayers)
        builder.string "batch_players"
        builder.string "data"
        result.to_json(builder)
      elsif result.is_a?(Job::Error)
        builder.string "error"
        builder.field("message", result.message)
        builder.field("data", result.id)
      elsif result.is_a?(Exception)
        builder.string "error"
        builder.field("message", result.message)
      end
    end

class Handlers::WebRouter
  include HTTP::Handler

  def initialize
    @logger = Log.for("web")
    @client = Steam::Client.new(ENV["STEAM_API_KEY"], @logger.for("Steam"))

    JobController.logger = @logger.for("JobController")
  end
end

logger = Logger.new(STDOUT)
client = Steam::Client.new(ENV["STEAM_API_KEY"], logger)
JobController.logger = logger

get "/" do |ctx|
  render("views/index.slang")
end

post "/api/check" do |ctx|
  # TODO: validate request
  raw_ids = ctx.query["steamids"].split(',')

  # ctx.halt({"error": ""}, 400) if !raw_ids || raw_ids == ""

  nonce = JobController.create(raw_ids.size + 1) do |job|
    ids = [] of Steam::ID
    raw_ids.each do |string_id|
      begin
        id = Steam::ID.new(string_id)
        # Enforce Public universe bit (STEAM_1..) and Individual account type:
        id.universe = :public
        id.account_type = :individual
        id.instance = 1

        ids << id
      rescue ex : Steam::ID::Error
        job.send Job::Error.new(string_id, ex.message)
      end

  def handle_check(context : HTTP::Server::Context)
    if context.request.method != "POST"
      context.response.respond_with_status(:method_not_allowed)
      return
    end

    # TODO: check if more than 100, maybe do this in middleware
    # for request validation
    players = client.get_players(ids)
    player_ids = players.map &.id

    ids.each do |id|
      unless player_ids.includes? id
        job.send Job::Error.new(
          id.to_s(Steam::ID::Format::Default),
          "Steam ID not found: #{id.to_s(Steam::ID::Format::Default)}"
        )
    @logger.info { "Handling check endpoint!" }
    raw_ids = context.request.query_params["steamids"].split(',')

    # ctx.halt({"error": ""}, 400) if !raw_ids || raw_ids == ""

    nonce = JobController.create(raw_ids.size + 1) do |job|
      ids = [] of Steam::ID
      raw_ids.each do |string_id|
        begin
          id = Steam::ID.new(string_id)
          # Enforce Public universe bit (STEAM_1..) and Individual account type:
          id.universe = :public
          id.account_type = :individual
          id.instance = 1

          ids << id
        rescue ex : Steam::ID::Error
          job.send Job::Error.new(string_id, ex.message)
        end
      end
    end

    lender_ids = [] of Steam::ID
      # TODO: check if more than 100, maybe do this in middleware
      # for request validation
      players = @client.get_players(ids)
      player_ids = players.map &.id

    players.each do |player|
      lender_id = client.get_lender_id(player.id)
      lender_ids << lender_id unless lender_id.nil?
      invalid_ids = [] of Steam::ID
      ids.each do |id|
        unless player_ids.includes? id
          invalid_ids << id
          job.send Job::Error.new(
                id.to_s(Steam::ID::Format::Default),
                "Not found"
              )
        end
      end

      job.send Job::PlayerResult.new(player, lender_id)
    end
      lender_ids = [] of Steam::ID

    unless lender_ids.empty?
      lenders = client.get_players(lender_ids)
      job.send Job::BatchPlayers.new(lenders)
    else
      job.send Job::BatchPlayers.new
      players.each do |player|
        lender_id = @client.get_lender_id(player.id)
        lender_ids << lender_id unless lender_id.nil?

        job.send Job::PlayerResult.new(player, lender_id)
      end

      unless lender_ids.empty?
        lenders = @client.get_players(lender_ids)
        job.send Job::BatchPlayers.new(lenders)
      else
        job.send Job::BatchPlayers.new
      end
    end

    context.response.content_type = "application/json"
    context.response.puts({nonce: nonce}.to_json)
  end

  {nonce: nonce}.to_json
end

# WS Payloads (spec for each of these):
# {"nonce": 123, "type": "error", "message": "bad id"}
# {"nonce": 123, "type": "result", "data": {"player": {}, "lender_id": "123"}}
# {"nonce": 123, "type": "result", "data": {"player": {}, "lender_id": null}}
ws "/api/relay" do |ws, ctx|
  ws.on_message do |message|
    # Client sends: {"nonce": 123}
    nonce = JobController::Nonce.from_json(message, "nonce")
    JobController.dispatch(nonce) do |result|
      payload = serialize(nonce, result)
      ws.send(payload)
  def call(context : HTTP::Server::Context)
    case context.request.path
    when "/" then context.response.print(Kilt.render("views/index.slang"))
    when "/api/check" then handle_check(context)
    else
      call_next(context)
    end
  rescue ex : JSON::ParseException | KeyError
    payload = serialize(nonce, ex)
    ws.send(payload)
  end
end
end
\ No newline at end of file

M src/run.cr => src/run.cr +22 -2
@@ 1,3 1,23 @@
require "./web"
require "http/server"
require "log"
require "./steam"
require "./mappings"
require "./job_controller"
require "./handlers/web_router"
require "./handlers/socket_handler"

Raze.run
server = HTTP::Server.new([
  HTTP::ErrorHandler.new,
  HTTP::LogHandler.new,
  HTTP::StaticFileHandler.new("static",
                              directory_listing: false),
  Handlers::WebRouter.new,
  Handlers::SocketHandler.new
])

backend = Log::IOBackend.new
Log.builder.bind "*", :debug, backend

Log.info { "Starting server..." }
server.bind_tcp "127.0.0.1", 8080
server.listen