diff --git a/README.md b/README.md
deleted file mode 100644
index ef40b5a..0000000
--- a/README.md
+++ /dev/null
@@ -1,120 +0,0 @@
-
-

-
- ### RobloxServer é um servidor privado open-source do Roblox 2013!
-
-
-
-
-## Procurando a documentação? Clique [aqui](docs/welcome.md).
-
-### Rodando no seu PC (Windows) em 1 minuto
-
-> O `127.0.0.1:8081` que aparece nos logs do GitHub Actions é uma máquina temporária do GitHub, usada só
-> para testar e apagada logo depois. O site precisa rodar no **seu** computador.
-
-1. Baixe o site já compilado: aba **Actions** → última execução verde → **Artifacts** → `RobloxServer-site`
- (ou clone o repositório e tenha o Visual Studio / Build Tools instalado, o script compila sozinho).
-2. Instale o [IIS Express](https://www.microsoft.com/download/details.aspx?id=48264) se ainda não tiver.
-3. Dê dois cliques em **`IniciarServidor.exe`** (ou no antigo `iniciar-servidor.bat`). O navegador abre `http://localhost:8080/`.
-4. Crie a primeira conta em *Sign Up* (ela vira administradora) e publique um jogo em *Build*.
-5. Coloque os clientes em `RobloxServer.Web/App_Data/Clients/2012M.zip` e `2013M.zip`, clique em **Download ROBLOX**,
- rode o launcher uma vez e depois é só clicar em **Play** na página do jogo. Os servidores são criados pelo bot HOST (veja [Modo bot](#modo-bot-servidores-247)).
-
-O **`IniciarServidor.exe`** abre uma janela com o estado do site e:
-
-* inicia o site no IIS Express (compila antes, se precisar) e o reinicia sozinho se ele fechar;
-* **mantém o computador ligado**: enquanto estiver aberto, o Windows não entra em suspensão (a tela ainda pode apagar);
-* pode **iniciar junto com o Windows** (já minimizado, perto do relógio) e fica na área de notificação quando minimizado.
- Uso: `IniciarServidor.exe [--port 8080] [--minimized]`.
-
-* com **Aceitar outros PCs (Radmin VPN / rede local)** marcado, amigos entram pelo seu IP do Radmin (`http://26.x.x.x:8080/`)
- ou da rede. O Windows pede permissão de administrador uma vez para liberar a porta do site e a UDP 53640 no firewall.
-
-Sem essa opção, o IIS Express só atende este PC (quem vem de fora vê *Bad Request - Invalid Hostname*).
-Para a Internet e o Raspberry Pi, use o IIS completo: [docs/setting-up.md](docs/setting-up.md).
-
-RobloxServer agora é um site **ASP.NET clássico (Web Forms, .NET Framework 4.6, C# 6 / Visual Studio 2015)**,
-com handlers `.ashx` e páginas `.aspx` como o roblox.com de 2015. Todo o JavaScript (Node/Express) foi removido.
-
-Os jogos publicados aqui são jogados pelo **[RobloxPlayerLauncher](https://github.com/V0rtexLinux/RobloxServerLauncher)**,
-o launcher oficial do site no estilo de 2013 (botão **Play** → o jogo abre), só com os clientes **2012M** e **2013M**.
-Um **Raspberry Pi Zero 2W** faz o port forwarding, o UPnP e o DNS da rede. Veja [docs/playing.md](docs/playing.md).
-
-### Estrutura
-
-| Pasta | O que tem |
-| --- | --- |
-| `RobloxServer.Web/` | O site ASP.NET (abra `RobloxServer.sln` no Visual Studio 2015 ou mais novo) |
-| `RobloxServer.Web/Game`, `Asset`, `Login`, ... | Handlers `.ashx` que os clientes 2015 chamam (`/Game/Visit.ashx`, `/asset/?id=`, ...) |
-| `RobloxServer.Web/Api` | JSON usado pelo launcher (jogos, servidores) |
-| `RobloxServer.Web/Install` | `/install/version.ashx` e `/install/download.ashx`: clientes 2012M/2013M e o launcher |
-| `RobloxServer.Starter/` | `IniciarServidor.exe`: inicia o site no IIS Express e mantém o PC ligado |
-| `RobloxServer.Web/App_Data` | Templates dos scripts; em execução guarda usuários, places, chaves e logs |
-| `pi/` | Instalação do Raspberry Pi Zero 2W (port forwarding, UPnP, DNS, nginx, Mono opcional) |
-| `tests/smoke_test.py` | Teste de ponta a ponta usado no CI |
-
-### Recursos
-
-* Visual do roblox.com de 2013: barra azul com o logo vermelho, submenu preto, página de jogos com filtro por gênero e os ícones originais (gênero, "no gear", 13+), botões verdes/azuis do StyleGuide e rodapé azul. As imagens ficam em `RobloxServer.Web/Images`.
-* Contas com cookie `.ROBLOSECURITY` (Forms Authentication, igual ao de 2015), cadastro com "sou menor de 13" (SuperSafeChat).
-* Publicação de jogos pelo site (`Develop.aspx`) ou pelo Studio 2015 (`/Data/Upload.ashx`).
-* `Visit.ashx`, `Join.ashx` e `GameServer.ashx` assinados com `--rbxsig` (RSA 1024 + SHA-1); os scripts de jogo 2012M/2013M
- (`App_Data/Templates/Join.lua` e `GameServer.lua`) seguem a conexão clássica do RBXPri/Novetus (licença MIT).
-* Botão **Play** que abre o RobloxPlayerLauncher (`robloxserver-player:`), com a janela "Starting ROBLOX...". Não há mais botão **Host Server**: quem hospeda é o [bot HOST](#modo-bot-servidores-247).
-* Clientes e launcher distribuídos pelo próprio site (`/install/`), com versão pelo SHA-256; a página Admin mostra o que está instalado.
-* `PlaceLauncher.ashx`, tickets de autenticação de uso único, `Login/Negotiate.ashx`, `ValidateTicket.ashx`.
-* `/GetAllowedMD5Hashes/` e `/GetAllowedSecurityVersions/` com `apiKey`.
-* Master server compatível com Novetus (`list.php`, `delist.php`, `serverlist.txt`).
-* Moderação estilo 2015: ban por tempo / "Account Deleted", página `NotApproved.aspx`, ban de IP, filtro de palavras.
-* Limite de requisições por IP (45 a cada 30 s, o mesmo do servidor antigo) e flood check no login.
-* Assets que não estão no servidor são redirecionados para o `assetdelivery.roblox.com`, como antes.
-
-Veja [docs/security-2015.md](docs/security-2015.md) para a lista completa das medidas de segurança (e das fraquezas mantidas de propósito).
-
-### Modo bot (servidores 24/7)
-
-Todo jogo publicado (os que já existem e os novos) ganha um servidor mantido por um bot chamado **HOST**, sem ninguém
-clicar em nada. O bot usa a mesma função do antigo botão *Host Server* (`GameStarter.Host` do launcher), só que sem entrar no jogo.
-
-* **Conta HOST:** criada sozinha na primeira vez, sem senha utilizável. Só ela pode hospedar (`PlaceService.CanHost`), e isso vale
- também no servidor, não só na tela. Crie a primeira conta (administradora) antes de iniciar os bots.
-* **Lista de jogos:** o launcher pergunta ao site (`Game/Servers.ashx?action=botlist`) a cada 60 s. Jogo novo ganha servidor em cerca de
- um minuto, jogo apagado perde o dele. Se o servidor cair, ou o site reiniciar e esquecer o job, o bot sobe de novo (com espera crescente).
-* **Portas UDP:** uma por jogo, a partir da `53641`, fixas em `App_Data/BotPorts.txt`. Libere-as no firewall e no roteador
- (o Cloudflare Tunnel leva só HTTP/HTTPS, não UDP). Para quem está fora da rede, defina `PublicGameAddress` no `Web.config`.
-* **Ajustes opcionais** em `App_Data/BotServers.txt` (uma linha cada):
- `placeId;porta;endereco` define porta e endereço de um jogo, `!placeId` desliga o bot desse jogo, `#` é comentário.
-* **Peso:** cada jogo é um cliente Roblox rodando 24/7. Em máquina fraca use `!placeId` nos jogos que não precisam ficar abertos.
-
-**Chave dos bots.** Nada secreto vai para o Git. Três formas, da mais simples à mais flexível:
-
-| Situação | Como |
-| --- | --- |
-| Launcher na **mesma máquina** do site | `RobloxPlayerLauncher.exe --bot-host --site-dir C:\caminho\RobloxServer.Web` (usa `http://localhost:8080/` se não passar `--site`). A chave aleatória fica em `App_Data/bot.key`, criada pelo site ou pelo launcher; apague o arquivo para trocar. |
-| Launcher em **outra máquina** | Pareamento único: logado como admin, abra `https://seusite/Game/Servers.ashx?action=botpaircode` (código de 10 min, uso único). Na outra máquina: `RobloxPlayerLauncher.exe --pair --site https://seusite/ --code CODIGO`. Depois: `RobloxPlayerLauncher.exe --bot-host --site https://seusite/`. |
-| Chave que você já tem | `RobloxPlayerLauncher.exe --set-key CHAVE` guarda a chave criptografada com o DPAPI do Windows (`%LocalAppData%\RobloxServer\bot.key`, só aquele usuário daquela máquina decifra). `--api-key CHAVE` também funciona, mas deixa a chave no atalho. |
-
-Cada máquina pareada recebe uma chave própria; o site guarda só o hash em `App_Data/bot-keys.txt`. Para revogar uma máquina, apague a linha dela.
-O pareamento exige `https://` (ou rede local), porque a chave viaja na resposta.
-
-**Deixar rodando 24/7:** o `IniciarServidor.exe` e o launcher são apps de janela e precisam de uma sessão do Windows aberta
-(não rodam bem como serviço). Use login automático e uma Tarefa Agendada "ao fazer logon" para cada um, e desligue a suspensão. O script `hospedagem/instalar-hospedagem.ps1` (PowerShell como Administrador) faz isso tudo: cria as duas tarefas, libera as portas UDP dos bots, desliga a suspensão (inclusive ao fechar a tampa) e, se você passar `-TunnelToken`, instala o Cloudflare Tunnel como serviço. `-Remove` desfaz.
-
-Para uma máquina **sem tela e sem teclado**, `hospedagem/pendrive/` monta um pendrive que instala o Windows 10 sozinho e já liga tudo (veja `montar-pendrive.ps1`). A senha do Windows nunca vai para o Git: ela é passada no script.
-
-### Rodando
-
-* **Windows / IIS:** [docs/setting-up.md](docs/setting-up.md)
-* **Raspberry Pi Zero 2W:** [docs/raspberry-pi.md](docs/raspberry-pi.md)
-
-### Downloads
-
-* Roblox Studio 2015 Version (January 14):
- + [~~Download Point 1 - Archive~~](https://archive.frickinfire.com/ROBLOX/Executables/Windows/RobloxStudio/ROBLOXArchive/2015/January%2014/)
- + [~~Download Point 2 - Ufile~~](https://ufile.io/2n226a2u)
- + [~~Download Point 3 - Mediafire~~](https://www.mediafire.com/file/sipe7u37cn3f3l8/January_14.tar/file)
- + [Download Point 4 - CatBox](https://files.catbox.moe/xdux95.7z)
-
-* Fiddler Classic (opcional, só para os executáveis oficiais de 2015; o DNS do Raspberry Pi cuida do resto):
- + [Download Point 1 - Telerik](https://www.telerik.com/download/fiddler)
diff --git a/RobloxServer.Web/Code/Config.cs b/RobloxServer.Web/Code/Config.cs
index 741e93e..84d798a 100644
--- a/RobloxServer.Web/Code/Config.cs
+++ b/RobloxServer.Web/Code/Config.cs
@@ -113,6 +113,18 @@ static string RequestBaseUrl()
/// RCC-style shared secret passed as ?apiKey= by game servers (2015 behaviour).
public static string ApiKey { get { return Get("ApiKey", ""); } }
+ /// Facebook app id (public). The secret comes from the ROBLOXSERVER_FACEBOOK_SECRET environment variable.
+ public static string FacebookAppId { get { return Get("FacebookAppId", ""); } }
+
+ public static string FacebookAppSecret
+ {
+ get
+ {
+ string fromEnvironment = System.Environment.GetEnvironmentVariable("ROBLOXSERVER_FACEBOOK_SECRET");
+ return !string.IsNullOrEmpty(fromEnvironment) ? fromEnvironment : Get("FacebookAppSecret", "");
+ }
+ }
+
public static bool RegistrationOpen { get { return GetBool("RegistrationOpen", true); } }
/// User names that are site administrators ("Roblox" staff).
diff --git a/RobloxServer.Web/Code/Data/Models.cs b/RobloxServer.Web/Code/Data/Models.cs
index 40348b7..928cda2 100644
--- a/RobloxServer.Web/Code/Data/Models.cs
+++ b/RobloxServer.Web/Code/Data/Models.cs
@@ -27,6 +27,9 @@ public class User
/// "Male", "Female" or null (2013 landing page signup).
public string Gender { get; set; }
+ /// Facebook user id when the account signs in with Facebook (null otherwise).
+ public string FacebookId { get; set; }
+
/// Set only if the user consented, at signup, to link their real Roblox username.
public bool RobloxVerified { get; set; }
diff --git a/RobloxServer.Web/Landing.aspx b/RobloxServer.Web/Landing.aspx
index 365ebb1..a09a9fe 100644
--- a/RobloxServer.Web/Landing.aspx
+++ b/RobloxServer.Web/Landing.aspx
@@ -54,6 +54,7 @@
diff --git a/RobloxServer.Web/Login.aspx b/RobloxServer.Web/Login.aspx
index ceb2df7..ef45ac5 100644
--- a/RobloxServer.Web/Login.aspx
+++ b/RobloxServer.Web/Login.aspx
@@ -26,6 +26,7 @@
Forgot your password? Ask an administrator.
diff --git a/RobloxServer.Web/Login/Facebook.ashx b/RobloxServer.Web/Login/Facebook.ashx
new file mode 100644
index 0000000..b864665
--- /dev/null
+++ b/RobloxServer.Web/Login/Facebook.ashx
@@ -0,0 +1 @@
+<%@ WebHandler Language="C#" CodeBehind="Facebook.ashx.cs" Class="RobloxServer.Handlers.Login.Facebook" %>
diff --git a/RobloxServer.Web/Login/Facebook.ashx.cs b/RobloxServer.Web/Login/Facebook.ashx.cs
new file mode 100644
index 0000000..e9e5efb
--- /dev/null
+++ b/RobloxServer.Web/Login/Facebook.ashx.cs
@@ -0,0 +1,239 @@
+using System;
+using System.Configuration;
+using System.IO;
+using System.Net;
+using System.Security.Cryptography;
+using System.Text;
+using System.Text.RegularExpressions;
+using System.Web;
+using RobloxServer.Data;
+using RobloxServer.Security;
+using RobloxServer.Web;
+
+namespace RobloxServer.Handlers.Login
+{
+ ///
+ /// "Login with Facebook" (OAuth 2, fluxo de codigo no servidor).
+ /// /Login/Facebook.ashx -> manda para o Facebook
+ /// /Login/Facebook.ashx?code=... -> volta do Facebook: entra, liga a conta logada, ou cria uma conta nova.
+ /// Precisa de FacebookAppId (Web.config) e do segredo na variavel de ambiente ROBLOXSERVER_FACEBOOK_SECRET.
+ ///
+ public class Facebook : HandlerBase
+ {
+ const string StateCookie = "RBXFbState";
+ const string Api = "https://graph.facebook.com/v19.0/";
+
+ static Facebook()
+ {
+ ServicePointManager.SecurityProtocol |= (SecurityProtocolType)3072; // TLS 1.2
+ }
+
+ protected override void Handle()
+ {
+ string appId = Config.FacebookAppId;
+ string secret = Config.FacebookAppSecret;
+ if (string.IsNullOrEmpty(appId) || string.IsNullOrEmpty(secret))
+ {
+ WriteStatus(503, "Facebook login is not configured on this server.");
+ return;
+ }
+ string code = Request.QueryString["code"];
+ if (string.IsNullOrEmpty(code))
+ {
+ if (!string.IsNullOrEmpty(Request.QueryString["error"]))
+ {
+ Fail("Facebook login was cancelled.");
+ return;
+ }
+ Start(appId);
+ return;
+ }
+ Finish(appId, secret, code);
+ }
+
+ string RedirectUri()
+ {
+ string root = ConfigurationManager.AppSettings["BaseUrl"];
+ if (string.IsNullOrEmpty(root))
+ {
+ root = Request.Url.GetLeftPart(UriPartial.Authority);
+ }
+ return root.TrimEnd('/') + "/Login/Facebook.ashx";
+ }
+
+ static string Hex(byte[] bytes)
+ {
+ return BitConverter.ToString(bytes).Replace("-", "").ToLowerInvariant();
+ }
+
+ static string RandomHex(int bytes)
+ {
+ var data = new byte[bytes];
+ using (var rng = new RNGCryptoServiceProvider())
+ {
+ rng.GetBytes(data);
+ }
+ return Hex(data);
+ }
+
+ void Start(string appId)
+ {
+ string state = RandomHex(16);
+ Response.Cookies.Add(new HttpCookie(StateCookie, state)
+ {
+ HttpOnly = true,
+ Secure = Request.IsSecureConnection,
+ Expires = DateTime.UtcNow.AddMinutes(10)
+ });
+ Response.Redirect("https://www.facebook.com/v19.0/dialog/oauth?client_id=" + Uri.EscapeDataString(appId)
+ + "&redirect_uri=" + Uri.EscapeDataString(RedirectUri()) + "&state=" + state + "&scope=public_profile", false);
+ }
+
+ void Fail(string text)
+ {
+ Response.Cookies.Add(new HttpCookie("RBXLoginError", HttpUtility.UrlEncode(text))
+ {
+ HttpOnly = true,
+ Expires = DateTime.UtcNow.AddMinutes(2)
+ });
+ Response.Redirect("~/Login.aspx", false);
+ }
+
+ static string Get(string url)
+ {
+ var request = (HttpWebRequest)WebRequest.Create(url);
+ request.Timeout = 15000;
+ request.ReadWriteTimeout = 15000;
+ using (var response = request.GetResponse())
+ using (var reader = new StreamReader(response.GetResponseStream(), Encoding.UTF8))
+ {
+ return reader.ReadToEnd();
+ }
+ }
+
+ void Finish(string appId, string secret, string code)
+ {
+ HttpCookie cookie = Request.Cookies[StateCookie];
+ string state = Request.QueryString["state"] ?? "";
+ if (cookie == null || string.IsNullOrEmpty(cookie.Value) || !PasswordHasher.ConstantTimeEquals(cookie.Value, state))
+ {
+ Fail("Facebook login expired. Please try again.");
+ return;
+ }
+ Response.Cookies.Add(new HttpCookie(StateCookie, "") { Expires = DateTime.UtcNow.AddDays(-1) });
+
+ string fbId;
+ string fbName;
+ try
+ {
+ string tokenJson = Get(Api + "oauth/access_token?client_id=" + Uri.EscapeDataString(appId)
+ + "&redirect_uri=" + Uri.EscapeDataString(RedirectUri())
+ + "&client_secret=" + Uri.EscapeDataString(secret) + "&code=" + Uri.EscapeDataString(code));
+ string token = Regex.Match(tokenJson, "\"access_token\"\s*:\s*\"([^\"]+)\"").Groups[1].Value;
+ if (token.Length == 0)
+ {
+ Fail("Facebook did not accept the login. Please try again.");
+ return;
+ }
+ string proof;
+ using (var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret)))
+ {
+ proof = Hex(hmac.ComputeHash(Encoding.UTF8.GetBytes(token)));
+ }
+ string me = Get(Api + "me?fields=id,name&access_token=" + Uri.EscapeDataString(token) + "&appsecret_proof=" + proof);
+ fbId = Regex.Match(me, "\"id\"\s*:\s*\"(\d+)\"").Groups[1].Value;
+ fbName = Regex.Match(me, "\"name\"\s*:\s*\"((?:[^\"\\\\]|\\\\.)*)\"").Groups[1].Value;
+ try { fbName = Regex.Unescape(fbName); } catch (ArgumentException) { }
+ }
+ catch (Exception ex)
+ {
+ Logging.Log(LogType.Security, "Facebook login failed: " + ex.Message);
+ Fail("Could not reach Facebook. Please try again.");
+ return;
+ }
+ if (fbId.Length == 0)
+ {
+ Fail("Facebook did not return your profile.");
+ return;
+ }
+
+ User user = Db.Users.Find(u => u.FacebookId == fbId);
+ if (user == null)
+ {
+ User current = Auth.CurrentUser;
+ if (current != null)
+ {
+ // Ja esta logado: so liga o Facebook a esta conta.
+ Db.Users.Update(u => u.Id == current.Id, u => u.FacebookId = fbId);
+ Response.Redirect("~/My/Home.aspx", false);
+ return;
+ }
+ user = CreateAccount(fbId, fbName);
+ if (user == null)
+ {
+ return;
+ }
+ }
+ Auth.SignIn(user, Context);
+ Response.Redirect(Accounts.AfterLogin(user, null), false);
+ }
+
+ User CreateAccount(string fbId, string fbName)
+ {
+ if (!Config.RegistrationOpen)
+ {
+ Fail("Registration is closed on this server.");
+ return null;
+ }
+ if (Db.Users.All().Count == 0)
+ {
+ Fail("Create the first account with a username and password before using Facebook.");
+ return null;
+ }
+ string ip = ClientIp.Get(Context);
+ if (FloodChecker.Hit("register:" + ip, 3, TimeSpan.FromHours(1)))
+ {
+ Fail("Too many accounts have been created from your IP. Try again later.");
+ return null;
+ }
+
+ string baseName = Regex.Replace(fbName ?? "", "[^A-Za-z0-9]", "");
+ if (baseName.Length < 3)
+ {
+ baseName = "Player";
+ }
+ if (baseName.Length > 12)
+ {
+ baseName = baseName.Substring(0, 12);
+ }
+ string chosen = baseName;
+ var random = new Random();
+ int tries = 0;
+ while (WordFilter.ValidateUserName(chosen) != null || Db.FindUser(chosen) != null)
+ {
+ if (++tries > 50)
+ {
+ Fail("Could not pick a username for you. Sign up with a username instead.");
+ return null;
+ }
+ chosen = baseName + random.Next(100, 99999);
+ }
+
+ string name = chosen;
+ DateTime now = DateTime.UtcNow;
+ string hash = PasswordHasher.Hash(RandomHex(32)); // senha aleatoria: a conta entra pelo Facebook
+ User created = Db.Users.InsertWithId(u => u.Id, 1, id => new User
+ {
+ Id = id,
+ Name = name,
+ PasswordHash = hash,
+ Created = now,
+ LastOnline = now,
+ LastIp = ip,
+ FacebookId = fbId
+ });
+ Logging.Log(LogType.Success, "New account " + created.Name + " (" + created.Id + ") via Facebook from " + ip);
+ return created;
+ }
+ }
+}
diff --git a/RobloxServer.Web/RobloxServer.Web.csproj b/RobloxServer.Web/RobloxServer.Web.csproj
index 8695413..b180eec 100644
--- a/RobloxServer.Web/RobloxServer.Web.csproj
+++ b/RobloxServer.Web/RobloxServer.Web.csproj
@@ -158,6 +158,7 @@
+
@@ -424,6 +425,9 @@
HeaderLogin.ashx
+
+ Facebook.ashx
+
Forum.aspx
ASPXCodeBehind