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.
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 tabelasMEMB_INFO,Charactere afins povoadas. - PHP 8.1 ou superior com os drivers
sqlsrvepdo_sqlsrvinstalados 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).
| Componente | Papel na arquitetura | Exemplo (varia por versão) |
|---|---|---|
| Site / Launcher / Bot | Cliente que consome a API | PHP, JS, Python |
| API REST | Camada de validação e acesso | PHP 8.x + sqlsrv |
| SQL Server | Persistência dos dados do jogo | MuOnline, porta 1433 |
| GameServer | Consome comandos e serve o jogo | MuServer 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.
- Suba a API e confirme que o front controller responde:
curl https://api.seuserver.com/?rota=ranking. - Verifique se o ranking retorna JSON válido e não um erro de conexão.
- Gere um token de escopo
admine guarde só o hash no banco. - 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. - Confirme na tabela
WEB_COMMAND_QUEUEque a linha foi criada comstatus = 0. - Rode o consumidor e verifique se o
statusmuda para1.
Erros comuns e soluções
| Erro | Causa provável | Solução |
|---|---|---|
Could not find driver | pdo_sqlsrv não habilitado no PHP | Confirme com php -m, ative no php.ini e reinicie o web server |
401 Token inválido sempre | Comparando token puro com hash no banco | Aplique hash('sha256', $token) antes de consultar |
Login failed for user 'api_mu' | Usuário SQL sem permissão ou senha errada | Recrie o login e conceda apenas os GRANT mínimos |
| Comando enfileirado mas nunca aplicado | Consumidor da fila parado | Verifique o serviço/agendador que processa WEB_COMMAND_QUEUE |
429 em uso normal | Limite de rate muito baixo | Ajuste limite/janela no RateLimiter conforme o tráfego real |
| Dados sensíveis no erro HTTP | Exceção do SQL vazando ao cliente | Capture Throwable, logue o detalhe e responda mensagem genérica |
| CORS bloqueando o site | Cabeçalhos ausentes | Defina 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_sqlsrvativos e testados - Usuário SQL dedicado com permissões mínimas (sem
sa, semdb_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
sha256no banco - Escopos de token definidos (leitura x admin)
- Allowlist de comandos implementada no
CommandController - Fila
WEB_COMMAND_QUEUEcriada 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.