diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..9e03c48 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,32 @@ +# Include any files or directories that you don't want to be copied to your +# container here (e.g., local build artifacts, temporary files, etc.). +# +# For more help, visit the .dockerignore file reference guide at +# https://docs.docker.com/go/build-context-dockerignore/ + +**/.DS_Store +**/.classpath +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/bin +**/charts +**/docker-compose* +**/compose.y*ml +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md diff --git a/.gitignore b/.gitignore index 026b94f..c05f5a1 100644 --- a/.gitignore +++ b/.gitignore @@ -362,4 +362,4 @@ MigrationBackup/ # Fody - auto-generated XML schema FodyWeavers.xsd -appsettings.production.json \ No newline at end of file +Certs \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..6a2da11 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,56 @@ +# syntax=docker/dockerfile:1 + +# Comments are provided throughout this file to help you get started. +# If you need more help, visit the Dockerfile reference guide at +# https://docs.docker.com/go/dockerfile-reference/ + +# Want to help us make this template better? Share your feedback here: https://forms.gle/ybq9Krt8jtBL3iCk7 + +################################################################################ + +# Learn about building .NET container images: +# https://github.com/dotnet/dotnet-docker/blob/main/samples/README.md + +# Create a stage for building the application. +FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0-alpine AS build + +COPY . /source + +WORKDIR /source/PurrLobby + +# This is the architecture you’re building for, which is passed in by the builder. +# Placing it here allows the previous steps to be cached across architectures. +ARG TARGETARCH + +# Build the application. +# Leverage a cache mount to /root/.nuget/packages so that subsequent builds don't have to re-download packages. +# If TARGETARCH is "amd64", replace it with "x64" - "x64" is .NET's canonical name for this and "amd64" doesn't +# work in .NET 6.0. +RUN --mount=type=cache,id=nuget,target=/root/.nuget/packages \ + dotnet publish -a ${TARGETARCH/amd64/x64} --use-current-runtime --self-contained false -o /app + +# If you need to enable globalization and time zones: +# https://github.com/dotnet/dotnet-docker/blob/main/samples/enable-globalization.md +################################################################################ +# Create a new stage for running the application that contains the minimal +# runtime dependencies for the application. This often uses a different base +# image from the build stage where the necessary files are copied from the build +# stage. +# +# The example below uses an aspnet alpine image as the foundation for running the app. +# It will also use whatever happens to be the most recent version of that tag when you +# build your Dockerfile. If reproducibility is important, consider using a more specific +# version (e.g., aspnet:7.0.10-alpine-3.18), +# or SHA (e.g., mcr.microsoft.com/dotnet/aspnet@sha256:f3d99f54d504a21d38e4cc2f13ff47d67235efeeb85c109d3d1ff1808b38d034). +FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine AS final +WORKDIR /app + +# Copy everything needed to run the app from the "build" stage. +COPY --from=build /app . + +# Switch to a non-privileged user (defined in the base image) that the app will run under. +# See https://docs.docker.com/go/dockerfile-user-best-practices/ +# and https://github.com/dotnet/dotnet-docker/discussions/4764 +USER $APP_UID + +ENTRYPOINT ["dotnet", "PurrLobby.dll"] diff --git a/PurrLobby/Models/Lobby.cs b/PurrLobby/Models/Lobby.cs index 6c36ad6..975cebd 100644 --- a/PurrLobby/Models/Lobby.cs +++ b/PurrLobby/Models/Lobby.cs @@ -5,6 +5,7 @@ public class LobbyUser { public required string Id { get; init; } public required string DisplayName { get; init; } + public int userPing { get; set; } public bool IsReady { get; set; } } diff --git a/PurrLobby/Pages/Index.cshtml b/PurrLobby/Pages/Index.cshtml new file mode 100644 index 0000000..edd78f5 --- /dev/null +++ b/PurrLobby/Pages/Index.cshtml @@ -0,0 +1,90 @@ +@page +@model PurrLobby.Pages.IndexModel +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers + + + + + + PurrLobby + + + + +
+
+ +

PurrLobby

+
+ +

Global Stats

+
+
+
@Model.GlobalPlayers
+
Players
+
+
+
@Model.GlobalLobbies
+
Lobbies
+
+
+ +

Track Your Game

+
+ + +
+ @Model.CookieStatus + +

Game Stats

+
+
+
@Model.GamePlayers
+
Players
+
+
+
@Model.GameLobbies
+
Lobbies
+
+
+ +
+ Swagger API Docs +
+ +

+ This Service does not (yet) belong to purrnet, it's just something I put together for an alternative to the Steam Lobby Service and Unity Service. + Since it's running on my own setup, it might go down or act weird sometimes. + If that happens, no worries, it'll probably be back soon. + Please don't contact the actual PurrNet devs about this, it's all on me. + If there are any problems contact me on discord: exil_s +

+
+ + diff --git a/PurrLobby/Pages/Index.cshtml.cs b/PurrLobby/Pages/Index.cshtml.cs new file mode 100644 index 0000000..716bac2 --- /dev/null +++ b/PurrLobby/Pages/Index.cshtml.cs @@ -0,0 +1,91 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using System.Net.Http; +using System.Text.Json; +using System.Threading.Tasks; + +namespace PurrLobby.Pages +{ + public class IndexModel : PageModel + { + [BindProperty] + public string GameIdInput { get; set; } = string.Empty; + public string CookieStatus { get; set; } = string.Empty; + public int GlobalPlayers { get; set; } = 0; + public int GlobalLobbies { get; set; } = 0; + public int GamePlayers { get; set; } = 0; + public int GameLobbies { get; set; } = 0; + + public async Task OnGetAsync() + { + await LoadStatsAsync(); + } + + public async Task OnPostAsync() + { + if (!Guid.TryParse(GameIdInput, out var gameId)) + { + CookieStatus = "Invalid Game ID format."; + await LoadStatsAsync(); + return Page(); + } + // Set gameId cookie via backend API + try + { + using var client = new HttpClient(); + var response = await client.PostAsync($"{Request.Scheme}://{Request.Host}/session/game", new StringContent(JsonSerializer.Serialize(new { GameId = gameId }), System.Text.Encoding.UTF8, "application/json")); + if (response.IsSuccessStatusCode) + { + CookieStatus = "Game ID set!"; + } + else + { + CookieStatus = "Failed to set Game ID."; + } + } + catch + { + CookieStatus = "Error contacting backend."; + } + await LoadStatsAsync(gameId); + return Page(); + } + + private async Task LoadStatsAsync(Guid? gameId = null) + { + using var client = new HttpClient(); + try + { + var globalPlayersResp = await client.GetStringAsync($"{Request.Scheme}://{Request.Host}/stats/global/players"); + GlobalPlayers = int.TryParse(globalPlayersResp, out var gp) ? gp : 0; + } + catch { GlobalPlayers = 0; } + try + { + var globalLobbiesResp = await client.GetStringAsync($"{Request.Scheme}://{Request.Host}/stats/global/lobbies"); + GlobalLobbies = int.TryParse(globalLobbiesResp, out var gl) ? gl : 0; + } + catch { GlobalLobbies = 0; } + if (gameId.HasValue) + { + try + { + var gamePlayersResp = await client.GetStringAsync($"{Request.Scheme}://{Request.Host}/stats/{gameId}/players"); + GamePlayers = int.TryParse(gamePlayersResp, out var gp) ? gp : 0; + } + catch { GamePlayers = 0; } + try + { + var gameLobbiesResp = await client.GetStringAsync($"{Request.Scheme}://{Request.Host}/stats/{gameId}/lobbies"); + GameLobbies = int.TryParse(gameLobbiesResp, out var gl) ? gl : 0; + } + catch { GameLobbies = 0; } + } + else + { + GamePlayers = 0; + GameLobbies = 0; + } + } + } +} diff --git a/PurrLobby/Program.cs b/PurrLobby/Program.cs index 23200d8..b76f1d4 100644 --- a/PurrLobby/Program.cs +++ b/PurrLobby/Program.cs @@ -86,6 +86,7 @@ builder.Services.AddSwaggerGen(o => }); builder.Services.AddSingleton(); builder.Services.AddSingleton(); +builder.Services.AddRazorPages(); // Register Razor Pages services var app = builder.Build(); if (!app.Environment.IsDevelopment()) { @@ -102,10 +103,10 @@ app.UseResponseCompression(); app.UseHttpLogging(); app.UseRateLimiter(); app.UseWebSockets(); -app.UseDefaultFiles(); app.UseStaticFiles(); app.UseSwagger(); app.UseSwaggerUI(); +app.MapRazorPages(); // Enable Razor Pages static CookieOptions BuildStdCookieOptions() => new() { HttpOnly = true, @@ -506,4 +507,4 @@ public record CreateLobbyRequest(string OwnerDisplayName, int MaxPlayers, Dictio public record JoinLobbyRequest(string DisplayName); public record ReadyRequest(string UserId, bool IsReady); public record AllReadyRequest(string UserId); -public record LobbyDataRequest(string Key, string Value); +public record LobbyDataRequest(string Key, string Value); \ No newline at end of file diff --git a/PurrLobby/PurrLobby.csproj b/PurrLobby/PurrLobby.csproj index 819bfa0..5aa6247 100644 --- a/PurrLobby/PurrLobby.csproj +++ b/PurrLobby/PurrLobby.csproj @@ -26,4 +26,14 @@ + + + + PreserveNewest + + + PreserveNewest + + + diff --git a/PurrLobby/appsettings.Production.json b/PurrLobby/appsettings.Production.json deleted file mode 100644 index ef912cb..0000000 --- a/PurrLobby/appsettings.Production.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "Kestrel": { - "Endpoints": { - "Https": { - "Url": "https://0.0.0.0:443", - "Certificate": { - "Path": "pemfile", - "KeyPath": "keyfile" - } - } - } - }, - "AllowedHosts": "your.domain.com" -} diff --git a/PurrLobby/appsettings.json b/PurrLobby/appsettings.json new file mode 100644 index 0000000..5c63240 --- /dev/null +++ b/PurrLobby/appsettings.json @@ -0,0 +1,20 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "purrlobby.exil.dev", + "Kestrel": { + "Endpoints": { + "Https": { + "Url": "https://0.0.0.0:443", + "Certificate": { + "Path": "Certs/purrlobby.pem", + "KeyPath": "Certs/purrlobby.key" + } + } + } + } +} \ No newline at end of file diff --git a/PurrLobby/wwwroot/index.html b/PurrLobby/wwwroot/index.html deleted file mode 100644 index 60914e8..0000000 --- a/PurrLobby/wwwroot/index.html +++ /dev/null @@ -1,63 +0,0 @@ - - - - - - PurrLobby - - - - -
-
- -

PurrLobby

-
- -

Global Stats

-
-
-
-
-
Players
-
-
-
-
-
Lobbies
-
-
- -

Track Your Game

-
- - -
- - -

Game Stats

-
-
-
-
-
Players
-
-
-
-
-
Lobbies
-
-
- - - -

- This Service is does not (yet) belong to purrnet, it's just something I put together for a alternative to the Steam Lobby Service and Unity Service. - Since it's running on my own setup, it might go down or act weird sometimes. - If that happens, no worries, it'll probably be back soon. - Please don't contact the actual PurrNet devs about this, it's all on me. - If there are any proplem's contact me on discord: exil_s -

-
- - - - diff --git a/PurrLobby/wwwroot/main.js b/PurrLobby/wwwroot/main.js deleted file mode 100644 index d150578..0000000 --- a/PurrLobby/wwwroot/main.js +++ /dev/null @@ -1,80 +0,0 @@ -async function fetchJson(url) { - const r = await fetch(url, { credentials: 'include' }); - if (!r.ok) throw new Error(`Request failed: ${r.status}`); - return r.json(); -} - -async function loadGlobal() { - try { - const [players, lobbies] = await Promise.all([ - fetchJson('/stats/global/players'), - fetchJson('/stats/global/lobbies') - ]); - document.getElementById('globalPlayers').textContent = players; - document.getElementById('globalLobbies').textContent = lobbies; - } catch (e) { - console.error(e); - } -} - -async function loadGame(gameId) { - try { - const [lobbies, players] = await Promise.all([ - fetchJson(`/stats/${gameId}/lobbies`), - fetchJson(`/stats/${gameId}/players`) - ]); - document.getElementById('gameLobbies').textContent = lobbies; - document.getElementById('gamePlayers').textContent = players.length ?? players; // endpoint returns list - } catch (e) { - console.error(e); - } -} - -async function setGameCookie(gameId) { - const r = await fetch('/session/game', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ gameId }), - credentials: 'include' - }); - if (!r.ok) throw new Error('Failed to set game cookie'); - return r.json(); -} - -function getCookie(name) { - const m = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)')); - return m ? decodeURIComponent(m[2]) : null; -} - -(async () => { - await loadGlobal(); - - const gameForm = document.getElementById('gameForm'); - const gameIdInput = document.getElementById('gameIdInput'); - const cookieStatus = document.getElementById('cookieStatus'); - - //if cookie exists prefill and load stats - const existing = getCookie('gameId'); - if (existing) { - gameIdInput.value = existing; - loadGame(existing); - cookieStatus.textContent = `Using gameId from cookie.`; - } - - gameForm.addEventListener('submit', async (e) => { - e.preventDefault(); - const gameId = gameIdInput.value.trim(); - if (!/^\{?[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}\}?$/.test(gameId)) { - alert('Please enter a valid GUID'); - return; - } - try { - await setGameCookie(gameId); - cookieStatus.textContent = 'GameId stored in cookie.'; - await loadGame(gameId); - } catch (err) { - cookieStatus.textContent = 'Failed to set cookie'; - console.error(err); - } - }); -})(); diff --git a/PurrLobby/wwwroot/styles.css b/PurrLobby/wwwroot/styles.css deleted file mode 100644 index 36c9453..0000000 --- a/PurrLobby/wwwroot/styles.css +++ /dev/null @@ -1,115 +0,0 @@ -:root { - --bg: #000; - --text: #fff; - --muted: #aaa; - --border: #333; - --accent: #2aa9ff; -} - -* { - box-sizing: border-box; - margin: 0; - padding: 0; -} - -body { - background: var(--bg); - color: var(--text); - font-family: monospace, sans-serif; - line-height: 1.5; -} - -.container { - max-width: 800px; - margin: 20px auto; - padding: 0 12px; -} - -.hero { - display: flex; - align-items: center; - gap: 12px; - margin-bottom: 20px; -} - -.logo { - width: 64px; - height: 64px; - object-fit: contain; -} - -.title { - font-size: 32px; -} - -.section-title { - font-size: 14px; - margin: 20px 0 8px; - text-transform: uppercase; -} - -.center { - text-align: center; -} - -.bigstats { - display: flex; - gap: 20px; - justify-content: center; - flex-wrap: wrap; - margin-bottom: 20px; -} - -.bigstat { - min-width: 120px; - display: flex; - flex-direction: column; - align-items: center; -} - -.bignum { - font-size: 32px; - font-weight: bold; - text-align: center; -} - -.biglabel { - font-size: 12px; - color: var(--muted); - text-align: center; -} - -.stack { - display: grid; - gap: 8px; - margin-bottom: 20px; -} - -#gameIdInput { - padding: 8px; - border: 1px solid var(--border); - background: var(--bg); - color: var(--text); -} - -button { - padding: 8px; - border: 1px solid var(--border); - background: #111; - color: var(--text); - cursor: pointer; -} - - button:hover { - background: #222; - } - -.disclaimer { - font-size: 13px; - color: var(--muted); - margin-top: 20px; -} - -a { - color: var(--accent); -} diff --git a/README.Docker.md b/README.Docker.md new file mode 100644 index 0000000..4ec1f02 --- /dev/null +++ b/README.Docker.md @@ -0,0 +1,24 @@ +### Building and running your application + +When you're ready, start your application by running: +`docker compose up --build`. + +Your application will be available at http://localhost:443. + +### Deploying your application to the cloud + +First, build your image, e.g.: `docker build -t myapp .`. +If your cloud uses a different CPU architecture than your development +machine (e.g., you are on a Mac M1 and your cloud provider is amd64), +you'll want to build the image for that platform, e.g.: +`docker build --platform=linux/amd64 -t myapp .`. + +Then, push it to your registry, e.g. `docker push myregistry.com/myapp`. + +Consult Docker's [getting started](https://docs.docker.com/go/get-started-sharing/) +docs for more detail on building and pushing. + +### References +* [Docker's .NET guide](https://docs.docker.com/language/dotnet/) +* The [dotnet-docker](https://github.com/dotnet/dotnet-docker/tree/main/samples) + repository has many relevant samples and docs. \ No newline at end of file diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..4f0013c --- /dev/null +++ b/compose.yaml @@ -0,0 +1,51 @@ +# Comments are provided throughout this file to help you get started. +# If you need more help, visit the Docker Compose reference guide at +# https://docs.docker.com/go/compose-spec-reference/ + +# Here the instructions define your application as a service called "server". +# This service is built from the Dockerfile in the current directory. +# You can add other services your application may depend on here, such as a +# database or a cache. For examples, see the Awesome Compose repository: +# https://github.com/docker/awesome-compose +services: + server: + build: + context: . + target: final + ports: + - 443:8080 + network_mode: "host" +# The commented out section below is an example of how to define a PostgreSQL +# database that your application can use. `depends_on` tells Docker Compose to +# start the database before your application. The `db-data` volume persists the +# database data between container restarts. The `db-password` secret is used +# to set the database password. You must create `db/password.txt` and add +# a password of your choosing to it before running `docker compose up`. +# depends_on: +# db: +# condition: service_healthy +# db: +# image: postgres +# restart: always +# user: postgres +# secrets: +# - db-password +# volumes: +# - db-data:/var/lib/postgresql/data +# environment: +# - POSTGRES_DB=example +# - POSTGRES_PASSWORD_FILE=/run/secrets/db-password +# expose: +# - 5432 +# healthcheck: +# test: [ "CMD", "pg_isready" ] +# interval: 10s +# timeout: 5s +# retries: 5 + +# volumes: +# db-data: +# secrets: +# db-password: +# file: db/password.txt +