Files
EpinelPS/nksrv/Program.cs
2024-07-04 11:17:27 -04:00

406 lines
14 KiB
C#

using EmbedIO;
using EmbedIO.Actions;
using Google.Protobuf;
using Google.Protobuf.WellKnownTypes;
using nksrv.IntlServer;
using nksrv.LobbyServer;
using nksrv.Utils;
using Swan.Logging;
using System.IO;
using System.IO.Compression;
using System.Net;
using System.Net.Http.Headers;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Net.Http.Formatting;
using System.Net.Security;
using Swan.Parsers;
using System.Net.Sockets;
using Newtonsoft.Json.Linq;
using Swan;
using Google.Api;
using nksrv.StaticInfo;
namespace nksrv
{
internal class Program
{
public static readonly HttpClient AssetDownloader = new();
static async Task Main()
{
Logger.UnregisterLogger<ConsoleLogger>();
Logger.RegisterLogger(new GreatLogger());
Logger.Info("Initializing database");
JsonDb.Save();
Logger.Info("Loading static data");
await StaticDataParser.Load();
Logger.Info("Parsing static data");
await StaticDataParser.Instance.Parse();
Logger.Info("Initialize handlers");
LobbyHandler.Init();
Logger.Info("Starting server");
using var server = CreateWebServer();
await server.RunAsync();
}
private static WebServer CreateWebServer()
{
var cert = new X509Certificate2(new X509Certificate(AppDomain.CurrentDomain.BaseDirectory + @"site.pfx"));
var server = new WebServer(o => o
.WithUrlPrefixes("https://*:443", "http://*:80")
.WithMode(HttpListenerMode.EmbedIO).WithAutoLoadCertificate().WithCertificate(cert))
// First, we will configure our web server by adding Modules.
.WithLocalSessionManager()
.WithModule(new ActionModule("/route/", HttpVerbs.Any, HandleRouteData))
.WithModule(new ActionModule("/v1/", HttpVerbs.Any, LobbyHandler.DispatchSingle))
.WithModule(new ActionModule("/v2/", HttpVerbs.Any, IntlHandler.Handle))
.WithModule(new ActionModule("/prdenv/", HttpVerbs.Any, HandleAsset))
.WithModule(new ActionModule("/account/", HttpVerbs.Any, IntlHandler.Handle))
.WithModule(new ActionModule("/data/", HttpVerbs.Any, HandleDataEndpoint))
.WithModule(new ActionModule("/media/", HttpVerbs.Any, HandleAsset))
.WithModule(new ActionModule("/$batch", HttpVerbs.Any, HandleBatchRequests))
.WithModule(new ActionModule("/nikke_launcher", HttpVerbs.Any, HandleLauncherUI));
// Listen for state changes.
//server.StateChanged += (s, e) => $"WebServer New State - {e.NewState}".Info();
return server;
}
private static async Task HandleLauncherUI(IHttpContext ctx)
{
await ctx.SendStringAsync(@"<!DOCTYPE html>
<html>
<head>
<title>Private Nikke Server Launcher</title>
<style>
* {
color:white;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
</style>
</head>
<body>
<h1>What's new in Nikke Private Server<h1>
<p>This is the inital release, only story mode works except that you can't collect items and a few other things don't work.</p>
<p>In order to level up characters, you manually have to edit db.json</p>
</body>
</html>
", "text/html", Encoding.UTF8);
}
private static async Task HandleBatchRequests(IHttpContext ctx)
{
var theBytes = await PacketDecryption.DecryptOrReturnContentAsync(ctx);
// this actually uses gzip compression, unlike other requests.
using MemoryStream streamforparser = new(theBytes.Contents);
StreamContent content = new(streamforparser);
content.Headers.Remove("Content-Type");
content.Headers.TryAddWithoutValidation("Content-Type", ctx.Request.Headers["Content-Type"]);
// we have the form contents,
var multipart = await content.ReadAsMultipartAsync();
HttpClient cl = new();
// TODO: the server returns different boundary each time, looks like a GUID
List<byte> response = [.. Encoding.UTF8.GetBytes("--f5d5cf4d-5627-422f-b3c6-532f1a0cbc0a\r\n")];
int i = 0;
foreach (var item in multipart.Contents)
{
i++;
response.AddRange(Encoding.UTF8.GetBytes("Content-Type: application/http\r\n"));
response.AddRange(Encoding.UTF8.GetBytes($"Content-ID: {item.Headers.NonValidated["Content-ID"]}\r\n"));
response.AddRange(Encoding.UTF8.GetBytes("\r\n"));
var bin = await item.ReadAsByteArrayAsync();
var res = await SendReqLocalAndReadResponseAsync(bin);
if (res != null)
{
List<byte> ResponseWithBytes =
[
.. Encoding.UTF8.GetBytes("HTTP/1.1 200 OK\r\n"),
.. Encoding.UTF8.GetBytes($"Content-Type: application/octet-stream+protobuf\r\n"),
.. Encoding.UTF8.GetBytes($"Content-Length: {res.Length}\r\n"),
.. Encoding.UTF8.GetBytes($"\r\n"),
.. res,
];
response.AddRange([.. ResponseWithBytes]);
}
else
{
List<byte> ResponseWithBytes =
[ .. Encoding.UTF8.GetBytes("HTTP/1.1 404 Not Found\r\n"),
//.. Encoding.UTF8.GetBytes($"Content-Type: application/octet-stream+protobuf\r\n"),
.. Encoding.UTF8.GetBytes($"Content-Length: 0\r\n"),
.. Encoding.UTF8.GetBytes($"\r\n"),
];
}
// add boundary, also include http newline if there is binary content
if (i == multipart.Contents.Count)
response.AddRange(Encoding.UTF8.GetBytes("\r\n--f5d5cf4d-5627-422f-b3c6-532f1a0cbc0a--\r\n"));
else
response.AddRange(Encoding.UTF8.GetBytes("\r\n--f5d5cf4d-5627-422f-b3c6-532f1a0cbc0a\r\n"));
}
var responseBytes = response.ToArray();
File.WriteAllBytes("batch-response", responseBytes);
ctx.Response.ContentType = "multipart/mixed; boundary=\"f5d5cf4d-5627-422f-b3c6-532f1a0cbc0a\"";
ctx.Response.OutputStream.Write(responseBytes);
}
private static async Task HandleDataEndpoint(IHttpContext ctx)
{
// this endpoint does not appear to be needed, it is used for telemetry
if (ctx.RequestedPath == "/v1/dsr/query")
{
await WriteJsonStringAsync(ctx, "{\"ret\":0,\"msg\":\"\",\"status\":0,\"created_at\":\"0\",\"target_destroy_at\":\"0\",\"destroyed_at\":\"0\",\"err_code\":0,\"seq\":\"1\"}");
}
else
{
ctx.Response.StatusCode = 404;
}
}
public static string GetCachePathForPath(string path)
{
return AppDomain.CurrentDomain.BaseDirectory + "cache" + path;
}
private static async Task HandleAsset(IHttpContext ctx)
{
string targetFile = GetCachePathForPath(ctx.Request.RawUrl);
var targetDir = Path.GetDirectoryName(targetFile);
if (targetDir == null)
{
Logger.Error($"ERROR: Directory name cannot be null for request " + ctx.Request.RawUrl + ", file path is " + targetFile);
return;
}
Directory.CreateDirectory(targetDir);
if (!File.Exists(targetFile))
{
Logger.Info("Download " + targetFile);
// TODO: Ip might change
string @base = ctx.Request.RawUrl.StartsWith("/prdenv") ? "prdenv" : "media";
var requestUri = new Uri("https://43.132.66.200/" + @base + ctx.RequestedPath);
using var request = new HttpRequestMessage(HttpMethod.Get, requestUri);
request.Headers.TryAddWithoutValidation("host", "cloud.nikke-kr.com");
using var response = await AssetDownloader.SendAsync(request);
if (response.StatusCode == HttpStatusCode.OK)
{
using var fss = new FileStream(targetFile, FileMode.CreateNew);
await response.Content.CopyToAsync(fss, ctx.CancellationToken);
fss.Close();
}
else
{
Logger.Error("FAILED TO DOWNLOAD FILE: " + ctx.RequestedPath);
ctx.Response.StatusCode = 404;
return;
}
}
try
{
using var fss = new FileStream(targetFile, FileMode.Open, FileAccess.Read, FileShare.Read);
using var responseStream = ctx.OpenResponseStream();
if (ctx.RequestedPath.EndsWith(".mp4"))
{
ctx.Response.ContentType = "video/mp4";
}
else if (ctx.RequestedPath.EndsWith(".json"))
{
ctx.Response.ContentType = "application/json";
}
ctx.Response.StatusCode = 200;
//ctx.Response.ContentLength64 = fss.Length; // TODO: This causes chrome to download content very slowl
await fss.CopyToAsync(responseStream, ctx.CancellationToken);
fss.Close();
}
catch (Exception ex)
{
Logger.Error(ex.ToString());
}
}
private static async Task HandleRouteData(IHttpContext ctx)
{
if (ctx.RequestedPath.Contains("/route_config.json"))
{
await ctx.SendStringAsync(@"{
""Config"": [
{
""VersionRange"": {
""From"": ""122.8.19"",
""To"": ""122.8.20"",
""PackageName"": ""com.proximabeta.nikke""
},
""Route"": [
{
""WorldId"": 81,
""Name"": ""pub:live-jp"",
""Url"": ""https://jp-lobby.nikke-kr.com/"",
""Description"": ""JAPAN"",
""Tags"": []
},
{
""WorldId"": 82,
""Name"": ""pub:live-na"",
""Url"": ""https://us-lobby.nikke-kr.com/"",
""Description"": ""NA"",
""Tags"": []
},
{
""WorldId"": 83,
""Name"": ""pub:live-kr"",
""Url"": ""https://kr-lobby.nikke-kr.com/"",
""Description"": ""KOREA"",
""Tags"": []
},
{
""WorldId"": 84,
""Name"": ""pub:live-global"",
""Url"": ""https://global-lobby.nikke-kr.com/"",
""Description"": ""GLOBAL"",
""Tags"": []
},
{
""WorldId"": 85,
""Name"": ""pub:live-sea"",
""Url"": ""https://sea-lobby.nikke-kr.com/"",
""Description"": ""SEA"",
""Tags"": []
}
]
},
{
""VersionRange"": {
""From"": ""121.8.19"",
""To"": ""122.8.20"",
""PackageName"": ""com.gamamobi.nikke""
},
""Route"": [
{
""WorldId"": 91,
""Name"": ""pub:live-hmt"",
""Url"": ""https://hmt-lobby.nikke-kr.com/"",
""Description"": ""HMT"",
""Tags"": []
}
]
}
]
}", "application/json", Encoding.Default);
}
else
{
Console.WriteLine("ROUTE - Unknown: " + ctx.RequestedPath);
ctx.Response.StatusCode = 404;
}
}
private static async Task WriteJsonStringAsync(IHttpContext ctx, string data)
{
var bt = Encoding.UTF8.GetBytes(data);
ctx.Response.ContentEncoding = null;
ctx.Response.ContentType = "application/json";
ctx.Response.ContentLength64 = bt.Length;
await ctx.Response.OutputStream.WriteAsync(bt, ctx.CancellationToken);
await ctx.Response.OutputStream.FlushAsync();
}
private static (string key, string value) GetHeader(string line)
{
var pieces = line.Split([':'], 2);
return (pieces[0].Trim(), pieces[1].Trim());
}
private static async Task<byte[]?> SendReqLocalAndReadResponseAsync(byte[] bytes)
{
int line = 0;
var bodyStartStr = Encoding.UTF8.GetString(bytes);
string method;
string url = "";
string httpVer;
string authToken = "";
List<NameValueHeaderValue> headers = [];
int currentByte = 0;
foreach (var item in bodyStartStr.Split("\r\n"))
{
if (line == 0)
{
var parts = item.Split(" ");
method = parts[0];
url = parts[1];
httpVer = parts[2];
}
else if (item == null || string.IsNullOrEmpty(item))
{
currentByte += 2;
break;
}
else
{
var (key, value) = GetHeader(item);
headers.Add(new NameValueHeaderValue(key, value));
if (key == "Authorization")
{
authToken = value.Replace("Bearer ", "");
}
}
currentByte += 2 + item.Length;
line++;
}
byte[] body;
if (currentByte == bytes.Length)
{
// empty body
body = [];
}
else
{
// not empty body, TODO
File.WriteAllBytes("notemptybody", bytes);
body = bytes.Skip(currentByte).ToArray();
}
if (!url.StartsWith("/v1/"))
{
throw new NotImplementedException("handler for " + url + " not implemented");
}
url = url.Replace("/v1", "");
// find appropriate handler
Logger.Info("BATCH: /v1" + url);
foreach (var item in LobbyHandler.Handlers)
{
if (item.Key == url)
{
item.Value.Reset();
item.Value.Contents = body;
await item.Value.HandleAsync(authToken);
return item.Value.ReturnBytes;
}
}
Logger.Error("HANDLER NOT FOUND: " + url);
return null;
}
}
}