O maior portal de MU Online do Brasil — desde 2003
Tutorial Avançado Website

Como criar uma API entre o site e o GameServer no MU Online

Aprenda a construir uma API REST em PHP entre o site e o GameServer do MU Online para trocar dados com segurança, disparar comandos e sincronizar informações sem expor o banco SQL diretamente na web.

BR Bruno · Atualizado em 10 jul 2026 · ⏱ 24 min de leitura
Resposta rápida

Conectar o site PHP diretamente ao banco SQL Server do GameServer funciona, mas é frágil: você espalha credenciais do banco por vários scripts, expõe a porta 1433 quando o site fica em outra máquina e mistura regra de negócio com acesso a dados. Uma API REST resolve isso criando uma fronteira clara

Conectar o site PHP diretamente ao banco SQL Server do GameServer funciona, mas é frágil: você espalha credenciais do banco por vários scripts, expõe a porta 1433 quando o site fica em outra máquina e mistura regra de negócio com acesso a dados. Uma API REST resolve isso criando uma fronteira clara — o site fala HTTP com a API, e só a API fala SQL com o banco. Este tutorial mostra, passo a passo, como construir essa camada em PHP puro, com autenticação por token, validação de entrada, controle de taxa e um canal para disparar comandos ao GameServer. Os nomes de tabelas, portas e formatos de senha aparecem como exemplo e variam por versão e por distribuição do seu MuServer, então trate cada valor como algo a confirmar no seu ambiente. Se você ainda não montou a base do servidor, comece pelo guia de como criar um servidor de MU Online e volte aqui para a camada web.

Pré-requisitos

Antes de escrever uma linha de código, tenha o ambiente pronto. A API é a peça central da integração, então cada dependência precisa estar sólida.

  • GameServer e SQL Server funcionando — o banco (MuOnline, por exemplo) já criado, com as tabelas MEMB_INFO, Character e afins povoadas.
  • PHP 8.1 ou superior com os drivers sqlsrv e pdo_sqlsrv instalados e habilitados (php -m | grep sqlsrv).
  • Servidor web (Apache ou Nginx) com suporte a reescrita de URL, para rotear todas as requisições a um único ponto de entrada.
  • Certificado TLS válido para o domínio da API (Let's Encrypt resolve de graça).
  • Rede interna definida — de preferência, a API, o GameServer e o SQL Server na mesma VLAN, com o banco fechado para a internet.
  • Um usuário SQL dedicado para a API, com permissões mínimas (nunca use o sa).
ComponentePapel na arquiteturaExemplo (varia por versão)
Site / Launcher / BotCliente que consome a APIPHP, JS, Python
API RESTCamada de validação e acessoPHP 8.x + sqlsrv
SQL ServerPersistência dos dados do jogoMuOnline, porta 1433
GameServerConsome comandos e serve o jogoMuServer S6 / S12+

Arquitetura da solução

A ideia central é que nenhum cliente fale com o banco diretamente. Todo o tráfego passa por HTTP até a API, que autentica, valida e traduz para SQL.

CLIENTES                         API (PHP)                 BACKEND
┌───────────┐   HTTPS + token   ┌─────────────┐  sqlsrv  ┌────────────┐
│ Site web  │ ────────────────▶ │  Roteador   │ ───────▶ │ SQL Server │
│ Launcher  │                   │  Auth       │          │ (MuOnline) │
│ Bot Disc. │ ◀──────────────── │  Validação  │ ◀─────── └────────────┘
└───────────┘   JSON            │  Rate limit │                │
                                └──────┬──────┘          fila / socket
                                       │                        ▼
                                       └───── comando ────▶ GameServer

O GameServer entra em dois pontos: quando a API lê dados que ele gravou (rankings, status online) e quando a API precisa mandar uma ação de volta — dar um item, kickar um jogador, enviar VIP. A forma desse "mandar de volta" varia por versão: algumas builds oferecem um socket administrativo, outras dependem de uma tabela de fila que o GameServer varre.

Estrutura de pastas e roteamento

Organize a API para que exista um único ponto de entrada (index.php), o que facilita aplicar autenticação e CORS de forma central.

/api
├── public/
│   └── index.php        ← front controller (único ponto de entrada)
├── src/
│   ├── Database.php      ← conexão sqlsrv (singleton)
│   ├── Auth.php          ← validação de token
│   ├── RateLimiter.php   ← controle de taxa
│   └── Controllers/
│       ├── AccountController.php
│       ├── RankingController.php
│       └── CommandController.php
├── config.php           ← credenciais (fora do public!)
└── .htaccess

O .htaccess (Apache) redireciona tudo para o front controller:

RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^ index.php [QSA,L]

# Nunca servir arquivos sensíveis
<FilesMatch "\.(env|ini|log)$">
    Require all denied
</FilesMatch>

Configuração de conexão segura ao banco

Isole a conexão num único arquivo, fora da pasta pública, usando um usuário SQL com permissões mínimas.

<?php
// src/Database.php
final class Database
{
    private static ?PDO $pdo = null;

    public static function conn(): PDO
    {
        if (self::$pdo === null) {
            $cfg = require __DIR__ . '/../config.php';
            $dsn = "sqlsrv:Server={$cfg['host']};Database={$cfg['db']}";

            self::$pdo = new PDO($dsn, $cfg['user'], $cfg['pass'], [
                PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
                PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
                PDO::ATTR_EMULATE_PREPARES   => false, // prepares reais
            ]);
        }
        return self::$pdo;
    }
}

No SQL Server, crie o usuário da API com o mínimo necessário. Conceda SELECT, INSERT e UPDATE apenas nas tabelas que a API realmente toca — nunca db_owner.

-- Exemplo (varia por versão): usuário dedicado da API
CREATE LOGIN api_mu WITH PASSWORD = 'S3nh4_Forte_Da_API!2026';
CREATE USER api_mu FOR LOGIN api_mu;

GRANT SELECT, INSERT, UPDATE ON dbo.MEMB_INFO TO api_mu;
GRANT SELECT ON dbo.Character TO api_mu;
GRANT SELECT, INSERT, UPDATE ON dbo.MEMB_VIP TO api_mu;
-- Fila de comandos criada por você (ver adiante)
GRANT SELECT, INSERT, UPDATE ON dbo.WEB_COMMAND_QUEUE TO api_mu;

Autenticação por token

A API precisa saber quem está chamando. Para comunicação servidor-a-servidor (site → API), a abordagem mais simples e sólida é um token de aplicação enviado no cabeçalho Authorization. Para ações em nome de um jogador, gere um token de sessão após o login.

<?php
// src/Auth.php
final class Auth
{
    public static function exigirToken(): array
    {
        $header = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
        if (!preg_match('/Bearer\s+(.+)/', $header, $m)) {
            self::negar('Token ausente');
        }
        $token = trim($m[1]);

        $stmt = Database::conn()->prepare(
            "SELECT app_id, scope, expira_em
             FROM WEB_API_TOKEN
             WHERE token_hash = ? AND ativo = 1"
        );
        // Guarde sempre o HASH do token, nunca o token puro
        $stmt->execute([hash('sha256', $token)]);
        $row = $stmt->fetch();

        if (!$row) {
            self::negar('Token inválido');
        }
        if ($row['expira_em'] !== null && strtotime($row['expira_em']) < time()) {
            self::negar('Token expirado');
        }
        return $row; // contém escopo/permissões
    }

    public static function negar(string $msg): never
    {
        http_response_code(401);
        echo json_encode(['erro' => $msg]);
        exit;
    }
}

Guardar apenas o sha256 do token no banco é essencial: se o banco vazar, o atacante não consegue reconstruir os tokens em uso. O token puro só existe no momento da geração e no cliente que o recebeu.

Controle de taxa (rate limiting)

Sem limite de requisições, um único cliente pode inundar a API — seja por bug ou por ataque. Um limitador simples por IP e por token já filtra abuso grosseiro.

<?php
// src/RateLimiter.php
final class RateLimiter
{
    // Máximo de requisições por janela de tempo
    public static function verificar(string $chave, int $limite = 60, int $janela = 60): void
    {
        $dir  = sys_get_temp_dir() . '/api_rl';
        @mkdir($dir, 0700, true);
        $arq  = $dir . '/' . hash('sha256', $chave);

        $agora   = time();
        $dados   = @json_decode(@file_get_contents($arq), true) ?: ['inicio' => $agora, 'hits' => 0];

        if ($agora - $dados['inicio'] >= $janela) {
            $dados = ['inicio' => $agora, 'hits' => 0]; // nova janela
        }
        $dados['hits']++;

        if ($dados['hits'] > $limite) {
            http_response_code(429);
            header('Retry-After: ' . ($janela - ($agora - $dados['inicio'])));
            echo json_encode(['erro' => 'Limite de requisições excedido']);
            exit;
        }
        file_put_contents($arq, json_encode($dados), LOCK_EX);
    }
}

Em produção com múltiplos servidores, troque o arquivo por Redis, para que o contador seja compartilhado entre as instâncias.

Endpoint de leitura: ranking

Endpoints de leitura são os mais seguros de começar, pois não alteram nada. O controller lê a tabela Character e devolve JSON limpo.

<?php
// src/Controllers/RankingController.php
final class RankingController
{
    public function top(int $limite = 50): void
    {
        $limite = max(1, min($limite, 200)); // trava o intervalo

        $stmt = Database::conn()->prepare(
            "SELECT TOP (?) Name, Class, cLevel, Resets, ConnectStat
             FROM Character
             WHERE CtlCode = 0
             ORDER BY Resets DESC, cLevel DESC"
        );
        $stmt->execute([$limite]);

        $dados = array_map(function ($r) {
            return [
                'nome'   => $r['Name'],
                'classe' => (int) $r['Class'],
                'level'  => (int) $r['cLevel'],
                'resets' => (int) $r['Resets'],
                'online' => (bool) $r['ConnectStat'],
            ];
        }, $stmt->fetchAll());

        Response::json(['ranking' => $dados]);
    }
}

Passar o limite como parâmetro preparado e travá-lo entre 1 e 200 evita que alguém peça TOP 9999999 e derrube o banco.

Endpoint de escrita: enviar comando ao GameServer

Aqui está o ponto mais delicado. Quando o site precisa dar um item, ativar VIP ou kickar um jogador, a API não deve executar isso "na mão" no banco de forma arriscada. O padrão mais confiável e portátil é a fila de comandos: a API insere uma linha numa tabela e o GameServer (ou um serviço auxiliar) a consome. Isso desacopla os dois sistemas e deixa um rastro auditável.

Crie a tabela de fila:

-- Exemplo (varia por versão): fila de comandos web -> jogo
CREATE TABLE WEB_COMMAND_QUEUE (
    id          INT IDENTITY(1,1) PRIMARY KEY,
    conta       VARCHAR(10)  NOT NULL,
    comando     VARCHAR(50)  NOT NULL,  -- ex: 'ADD_VIP', 'GIVE_ITEM'
    parametros  NVARCHAR(MAX) NULL,     -- JSON com detalhes
    status      TINYINT      NOT NULL DEFAULT 0, -- 0=pendente 1=ok 2=erro
    criado_em   DATETIME     NOT NULL DEFAULT GETDATE(),
    processado_em DATETIME   NULL
);

O controller apenas valida e enfileira:

<?php
// src/Controllers/CommandController.php
final class CommandController
{
    private const COMANDOS_PERMITIDOS = ['ADD_VIP', 'GIVE_ITEM', 'KICK'];

    public function enfileirar(array $auth): void
    {
        $body = json_decode(file_get_contents('php://input'), true) ?: [];

        $conta   = trim($body['conta']   ?? '');
        $comando = strtoupper(trim($body['comando'] ?? ''));
        $params  = $body['parametros'] ?? [];

        // Validações rígidas
        if (!preg_match('/^[a-zA-Z0-9]{4,10}$/', $conta)) {
            Response::erro(422, 'Conta inválida');
        }
        if (!in_array($comando, self::COMANDOS_PERMITIDOS, true)) {
            Response::erro(422, 'Comando não permitido');
        }
        // O escopo do token precisa autorizar comandos
        if (($auth['scope'] ?? '') !== 'admin') {
            Response::erro(403, 'Sem permissão para comandos');
        }

        $stmt = Database::conn()->prepare(
            "INSERT INTO WEB_COMMAND_QUEUE (conta, comando, parametros)
             VALUES (?, ?, ?)"
        );
        $stmt->execute([$conta, $comando, json_encode($params)]);

        Response::json(['status' => 'enfileirado', 'id' => Database::conn()->lastInsertId()]);
    }
}

Um serviço leve (um script agendado ou um daemon) lê os registros pendentes e os aplica — seja gravando na tabela final do jogo, seja enviando via socket administrativo quando a sua versão de GameServer oferece esse canal. Manter a lista de comandos permitidos como uma constante (allowlist) impede que qualquer string vire uma ação executável.

Front controller e resposta padronizada

O index.php amarra tudo: define cabeçalhos, aplica rate limit, autentica e roteia.

<?php
// public/index.php
require __DIR__ . '/../src/Database.php';
require __DIR__ . '/../src/Auth.php';
require __DIR__ . '/../src/RateLimiter.php';
// ... require dos controllers

header('Content-Type: application/json; charset=utf-8');
header('X-Content-Type-Options: nosniff');

RateLimiter::verificar($_SERVER['REMOTE_ADDR'] ?? 'anon');

$rota   = $_GET['rota'] ?? '';
$metodo = $_SERVER['REQUEST_METHOD'];

try {
    switch ("$metodo $rota") {
        case 'GET ranking':
            (new RankingController())->top((int)($_GET['limite'] ?? 50));
            break;
        case 'POST comando':
            $auth = Auth::exigirToken();
            (new CommandController())->enfileirar($auth);
            break;
        default:
            Response::erro(404, 'Rota não encontrada');
    }
} catch (Throwable $e) {
    error_log('[API] ' . $e->getMessage());
    Response::erro(500, 'Erro interno');
}

Repare que a mensagem de erro devolvida ao cliente é genérica (Erro interno), enquanto o detalhe real vai só para o log. Vazar mensagens de exceção do SQL Server entrega ao atacante nomes de tabela e estrutura do banco.

Testando a API

Antes de plugar o site, teste cada endpoint isoladamente com curl. Assim você separa problemas da API de problemas do front-end.

  1. Suba a API e confirme que o front controller responde: curl https://api.seuserver.com/?rota=ranking.
  2. Verifique se o ranking retorna JSON válido e não um erro de conexão.
  3. Gere um token de escopo admin e guarde só o hash no banco.
  4. Teste o comando: curl -X POST -H "Authorization: Bearer SEU_TOKEN" -d '{"conta":"teste","comando":"ADD_VIP","parametros":{"dias":30}}' https://api.seuserver.com/?rota=comando.
  5. Confirme na tabela WEB_COMMAND_QUEUE que a linha foi criada com status = 0.
  6. Rode o consumidor e verifique se o status muda para 1.

Erros comuns e soluções

ErroCausa provávelSolução
Could not find driverpdo_sqlsrv não habilitado no PHPConfirme com php -m, ative no php.ini e reinicie o web server
401 Token inválido sempreComparando token puro com hash no bancoAplique hash('sha256', $token) antes de consultar
Login failed for user 'api_mu'Usuário SQL sem permissão ou senha erradaRecrie o login e conceda apenas os GRANT mínimos
Comando enfileirado mas nunca aplicadoConsumidor da fila paradoVerifique o serviço/agendador que processa WEB_COMMAND_QUEUE
429 em uso normalLimite de rate muito baixoAjuste limite/janela no RateLimiter conforme o tráfego real
Dados sensíveis no erro HTTPExceção do SQL vazando ao clienteCapture Throwable, logue o detalhe e responda mensagem genérica
CORS bloqueando o siteCabeçalhos ausentesDefina Access-Control-Allow-Origin para o domínio exato do site

Boas práticas de segurança

A API vira o portão de entrada do seu servidor, então trate-a como infraestrutura crítica. Nunca deixe o SQL Server aberto para a internet — a API deve ser o único que o alcança, pela rede interna. Use sempre prepared statements, sem exceção, mesmo em consultas que parecem inofensivas. Mantenha os tokens com escopo: um token que só lê ranking jamais deve conseguir enviar comandos. Rotacione tokens periodicamente e tenha um caminho para revogar (ativo = 0) qualquer token comprometido em segundos. Por fim, logue toda ação de escrita com data, IP e token de origem — quando algo der errado, esse rastro é a diferença entre resolver em minutos e ficar no escuro.

Checklist de lançamento

  • PHP com sqlsrv/pdo_sqlsrv ativos e testados
  • Usuário SQL dedicado com permissões mínimas (sem sa, sem db_owner)
  • SQL Server fechado para a internet, acessível só pela rede interna
  • HTTPS/TLS válido no domínio da API
  • Front controller único com rate limiting aplicado
  • Tokens guardados apenas como hash sha256 no banco
  • Escopos de token definidos (leitura x admin)
  • Allowlist de comandos implementada no CommandController
  • Fila WEB_COMMAND_QUEUE criada e consumidor rodando
  • Mensagens de erro genéricas ao cliente, detalhe só no log
  • Endpoints testados individualmente com curl
  • Log de auditoria para toda operação de escrita

Perguntas frequentes

Por que usar uma API em vez de conectar o site direto no SQL?

Uma API cria uma camada intermediária que valida, autentica e limita cada operação. Isso evita expor a porta 1433 do SQL Server na internet e reduz a superfície de ataque a um único endpoint controlado.

A API precisa rodar no mesmo servidor que o GameServer?

Não é obrigatório, mas é recomendado que ela fique na mesma rede interna do GameServer e do SQL Server. Assim o banco nunca precisa aceitar conexões externas e a API vira o único ponto de entrada.

Como o GameServer recebe comandos vindos do site?

Depende da versão. Muitos GameServers expõem um socket administrativo (ConnectServer/GS Live) ou leem uma tabela de fila no banco. A API grava o comando e o GameServer o consome, ou envia via socket quando a versão suporta.

Preciso de HTTPS na API?

Sim, sempre. Tokens e dados de conta trafegam nas requisições. Sem TLS qualquer intermediário na rede lê os tokens em texto claro e assume a identidade do site.

Posso reaproveitar essa API para um app mobile ou bot de Discord?

Sim. Esse é o maior benefício de uma API: qualquer cliente autorizado (site, launcher, app, bot) consome os mesmos endpoints com o mesmo modelo de autenticação, sem duplicar lógica de acesso ao banco.

BR
Editor de eventos, mapas e itens

Bruno é especialista em eventos, mapas, bosses e economia de itens do MU Online. Documenta cada detalhe com base em jogo real.

Continue lendo

Artigos relacionados