Add launcher catalog workflow and smoke tests
Some checks failed
Build / release (macos-latest) (push) Has been cancelled
Build / release (ubuntu-latest) (push) Has been cancelled
Build / release (windows-latest) (push) Has been cancelled
Windows Smoke Test / windows-smoke (push) Has been cancelled

This commit is contained in:
2026-05-04 14:06:05 +09:00
parent eb7ef9bbf2
commit 24a0569fb4
106 changed files with 24095 additions and 6 deletions

52
docs/MicrosoftAuth.md Normal file
View File

@@ -0,0 +1,52 @@
# Microsoft Authentication
Authenticating with Microsoft is fully supported by Helios Launcher.
## Acquiring an Entra Client ID
1. Navigate to https://portal.azure.com
2. In the search bar, search for **Microsoft Entra ID**.
3. In Microsoft Entra ID, go to **App Registrations** on the left pane (Under *Manage*).
4. Click **New Registration**.
- Set **Name** to be your launcher's name.
- Set **Supported account types** to *Accounts in any organizational directory (Any Microsoft Entra ID tenant - Multitenant) and personal Microsoft accounts (e.g. Skype, Xbox)*
- Leave **Redirect URI** blank.
- Register the application.
5. You should be on the application's management page. If not, Navigate back to **App Registrations**. Select the application you just registered.
6. Click **Authentication** on the left pane (Under *Manage*).
7. Click **Add Platform**.
- Select **Mobile and desktop applications**.
- Choose `https://login.microsoftonline.com/common/oauth2/nativeclient` as the **Redirect URI**.
- Select **Configure** to finish adding the platform.
8. Go to **Certificates & secrets**.
- Select **Client secrets**.
- Click **New client secret**.
- Set a description.
- Click **Add**.
- Don't copy the client secret, adding one is just a requirement from Microsoft.
8. Navigate back to **Overview**.
9. Copy **Application (client) ID**.
## Adding the Entra Client ID to Helios Launcher.
In `app/assets/js/ipcconstants.js` you'll find **`AZURE_CLIENT_ID`**. Set it to your application's id.
Note: Entra Client ID is NOT a secret value and **can** be stored in git. Reference: https://stackoverflow.com/questions/57306964/are-azure-active-directorys-tenantid-and-clientid-considered-secrets
Then relaunch your app, and login. You'll be greeted with an error message, because the app isn't whitelisted yet. Microsoft needs some activity on the app before whitelisting it. __Trying to log in before requesting whitelist is mandatory.__
## Requesting whitelisting from Microsoft
1. Ensure you have completed every step of this doc page.
2. Fill [this form](https://aka.ms/mce-reviewappid) with the required information. Remember this is a new appID for approval. You can find both the Client ID and the Tenant ID on the overview page in the Azure Portal.
3. Give Microsoft some time to review your app.
4. Once you have received Microsoft's approval, allow up to 24 hours for the changes to apply.
----
You can now authenticate with Microsoft through the launcher.
References:
- https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app
- https://help.minecraft.net/hc/en-us/articles/16254801392141

592
docs/distro.md Normal file
View File

@@ -0,0 +1,592 @@
# Distribution Index
You can use [Nebula](https://github.com/dscalzi/Nebula) to automate the generation of a distribution index.
The most up to date and accurate descriptions of the distribution spec can be viewed in [helios-distribution-types](https://github.com/dscalzi/helios-distribution-types).
The distribution index is written in JSON. The general format of the index is as posted below.
```json
{
"version": "1.0.0",
"discord": {
"clientId": "12334567890123456789",
"smallImageText": "WesterosCraft",
"smallImageKey": "seal-circle"
},
"rss": "https://westeroscraft.com/articles/index.rss",
"servers": [
{
"id": "Example_Server",
"name": "WesterosCraft Example Client",
"description": "Example WesterosCraft server. Connect for fun!",
"icon": "http://mc.westeroscraft.com/WesterosCraftLauncher/files/example_icon.png",
"version": "0.0.1",
"address": "mc.westeroscraft.com:1337",
"minecraftVersion": "1.11.2",
"discord": {
"shortId": "Example",
"largeImageText": "WesterosCraft Example Server",
"largeImageKey": "server-example"
},
"mainServer": true,
"autoconnect": true,
"modules": [
"Module Objects Here"
]
}
]
}
```
## Distro Index Object
#### Example
```JSON
{
"version": "1.0.0",
"discord": {
"clientId": "12334567890123456789",
"smallImageText": "WesterosCraft",
"smallImageKey": "seal-circle"
},
"rss": "https://westeroscraft.com/articles/index.rss",
"servers": []
}
```
### `DistroIndex.version: string/semver`
The version of the index format. Will be used in the future to gracefully push updates.
### `DistroIndex.discord: object`
Global settings for [Discord Rich Presence](https://discordapp.com/developers/docs/rich-presence/how-to).
**Properties**
* `discord.clientId: string` - Client ID for th Application registered with Discord.
* `discord.smallImageText: string` - Tootltip for the `smallImageKey`.
* `discord.smallImageKey: string` - Name of the uploaded image for the small profile artwork.
### `DistroIndex.rss: string/url`
A URL to a RSS feed. Used for loading news.
---
## Server Object
#### Example
```JSON
{
"id": "Example_Server",
"name": "WesterosCraft Example Client",
"description": "Example WesterosCraft server. Connect for fun!",
"icon": "http://mc.westeroscraft.com/WesterosCraftLauncher/files/example_icon.png",
"version": "0.0.1",
"address": "mc.westeroscraft.com:1337",
"minecraftVersion": "1.11.2",
"discord": {
"shortId": "Example",
"largeImageText": "WesterosCraft Example Server",
"largeImageKey": "server-example"
},
"mainServer": true,
"autoconnect": true,
"modules": []
}
```
### `Server.id: string`
The ID of the server. The launcher saves mod configurations and selected servers by ID. If the ID changes, all data related to the old ID **will be wiped**.
### `Server.name: string`
The name of the server. This is what users see on the UI.
### `Server.description: string`
A brief description of the server. Displayed on the UI to provide users more information.
### `Server.icon: string/url`
A URL to the server's icon. Will be displayed on the UI.
### `Server.version: string/semver`
The version of the server configuration.
### `Server.address: string/url`
The server's IP address.
### `Server.minecraftVersion: string`
The version of minecraft that the server is running.
### `Server.discord: object`
Server specific settings used for [Discord Rich Presence](https://discordapp.com/developers/docs/rich-presence/how-to).
**Properties**
* `discord.shortId: string` - Short ID for the server. Displayed on the second status line as `Server: shortId`
* `discord.largeImageText: string` - Ttooltip for the `largeImageKey`.
* `discord.largeImageKey: string` - Name of the uploaded image for the large profile artwork.
### `Server.mainServer: boolean`
Only one server in the array should have the `mainServer` property enabled. This will tell the launcher that this is the default server to select if either the previously selected server is invalid, or there is no previously selected server. If this field is not defined by any server (avoid this), the first server will be selected as the default. If multiple servers have `mainServer` enabled, the first one the launcher finds will be the effective value. Servers which are not the default may omit this property rather than explicitly setting it to false.
### `Server.autoconnect: boolean`
Whether or not the server can be autoconnected to. If false, the server will not be autoconnected to even when the user has the autoconnect setting enabled.
### `Server.javaOptions: JavaOptions`
**OPTIONAL**
Sever-specific Java options. If not provided, defaults are used by the client.
### `Server.modules: Module[]`
An array of module objects.
---
## JavaOptions Object
Server-specific Java options.
#### Example
```JSON
{
"supported": ">=17",
"suggestedMajor": 17,
"platformOptions": [
{
"platform": "darwin",
"architecture": "arm64",
"distribution": "CORRETTO"
}
],
"ram": {
"recommended": 3072,
"minimum": 2048
}
}
```
### `JavaOptions.platformOptions: JavaPlatformOptions[]`
**OPTIONAL**
Platform-specific java rules for this server configuration. Validation rules will be delegated to the client for any undefined properties. Java validation can be configured for specific platforms and architectures. The most specific ruleset will be applied.
Maxtrix Precedence (Highest - Lowest)
- Current platform, current architecture (ex. win32 x64).
- Current platform, any architecture (ex. win32).
- Java Options base properties.
- Client logic (default logic in the client).
Properties:
- `platformOptions.platform: string` - The platform that this validation matrix applies to.
- `platformOptions.architecture: string` - Optional. The architecture that this validation matrix applies to. If omitted, applies to all architectures.
- `platformOptions.distribution: string` - Optional. See `JavaOptions.distribution`.
- `platformOptions.supported: string` - Optional. See `JavaOptions.supported`.
- `platformOptions.suggestedMajor: number` - Optional. See `JavaOptions.suggestedMajor`.
### `JavaOptions.ram: object`
**OPTIONAL**
This allows you to require a minimum and recommended amount of RAM per server instance. The minimum is the smallest value the user can select in the settings slider. The recommended value will be the default value selected for that server. These values are specified in megabytes and must be an interval of 512. This allows configuration in intervals of half gigabytes. In the above example, the recommended ram value is 3 GB (3072 MB) and the minimum is 2 GB (2048 MB).
- `ram.recommended: number` - The recommended amount of RAM in megabytes. Must be an interval of 512.
- `ram.minimum: number` - The absolute minimum amount of RAM in megabytes. Must be an interval of 512.
### `JavaOptions.distribution: string`
**OPTIONAL**
Preferred JDK distribution to download if no applicable installation could be found. If omitted, the client will decide (decision may be platform-specific).
### `JavaOptions.supported: string`
**OPTIONAL**
A semver range of supported JDK versions.
Java version syntax is platform dependent.
JDK 8 and prior
```
1.{major}.{minor}_{patch}-b{build}
Ex. 1.8.0_152-b16
```
JDK 9+
```
{major}.{minor}.{patch}+{build}
Ex. 11.0.12+7
```
For processing, all versions will be translated into a semver compliant string. JDK 9+ is already semver. For versions 8 and below, `1.{major}.{minor}_{patch}-b{build}` will be translated to `{major}.{minor}.{patch}+{build}`.
If specified, you must also specify suggestedMajor.
If omitted, the client will decide based on the game version.
### `JavaOptions.suggestedMajor: number`
**OPTIONAL**
The suggested major Java version. The suggested major should comply with the version range specified by supported, if defined. This will be used in messages displayed to the end user, and to automatically fetch a Java version.
NOTE If supported is specified, suggestedMajor must be set. The launcher's default value may not comply with your custom major supported range.
Common use case:
- supported: '>=17.x'
- suggestedMajor: 17
More involved:
- supported: '>=16 <20'
- suggestedMajor: 17
Given a wider support range, it becomes necessary to specify which major version in the range is the suggested.
---
## Module Object
A module is a generic representation of a file required to run the minecraft client.
#### Example
```JSON
{
"id": "com.example:artifact:1.0.0@jar.pack.xz",
"name": "Artifact 1.0.0",
"type": "Library",
"artifact": {
"size": 4231234,
"MD5": "7f30eefe5c51e1ae0939dab2051db75f",
"url": "http://files.site.com/maven/com/example/artifact/1.0.0/artifact-1.0.0.jar.pack.xz"
},
"subModules": [
{
"id": "examplefile",
"name": "Example File",
"type": "File",
"artifact": {
"size": 23423,
"MD5": "169a5e6cf30c2cc8649755cdc5d7bad7",
"path": "examplefile.txt",
"url": "http://files.site.com/examplefile.txt"
}
}
]
}
```
The parent module will be stored maven style, it's destination path will be resolved by its id. The sub module has a declared `path`, so that value will be used.
### `Module.id: string`
The ID of the module. All modules that are not of type `File` **MUST** use a maven identifier. Version information and other metadata is pulled from the identifier. Modules which are stored maven style use the identifier to resolve the destination path. If the `extension` is not provided, it defaults to `jar`.
**Template**
`my.group:arifact:version@extension`
`my/group/artifact/version/artifact-version.extension`
**Example**
`net.minecraft:launchwrapper:1.12` OR `net.minecraft:launchwrapper:1.12@jar`
`net/minecraft/launchwrapper/1.12/launchwrapper-1.12.jar`
If the module's artifact does not declare the `path` property, its path will be resolved from the ID.
### `Module.name: string`
The name of the module. Used on the UI.
### `Module.type: string`
The type of the module.
### `Module.classpath: boolean`
**OPTIONAL**
If the module is of type `Library`, whether the library should be added to the classpath. Defaults to true.
### `Module.required: Required`
**OPTIONAL**
Defines whether or not the module is required. If omitted, then the module will be required.
Only applicable for modules of type:
* `ForgeMod`
* `LiteMod`
* `LiteLoader`
### `Module.artifact: Artifact`
The download artifact for the module.
### `Module.subModules: Module[]`
**OPTIONAL**
An array of sub modules declared by this module. Typically, files which require other files are declared as submodules. A quick example would be a mod, and the configuration file for that mod. Submodules can also declare submodules of their own. The file is parsed recursively, so there is no limit.
## Artifact Object
The format of the module's artifact depends on several things. The most important factor is where the file will be stored. If you are providing a simple file to be placed in the root directory of the client files, you may decided to format the module as the `examplefile` module declared above. This module provides a `path` option, allowing you to directly set where the file will be saved to. Only the `path` will affect the final downloaded file.
Other times, you may want to store the files maven-style, such as with libraries and mods. In this case you must declare the module as the example artifact above. The module `id` will be used to resolve the final path, effectively replacing the `path` property. It must be provided in maven format. More information on this is provided in the documentation for the `id` property.
The resolved/provided paths are appended to a base path depending on the module's declared type.
| Type | Path |
| ---- | ---- |
| `ForgeHosted` | ({`commonDirectory`}/libraries/{`path` OR resolved}) |
| `Fabric` | ({`commonDirectory`}/libraries/{`path` OR resolved}) |
| `LiteLoader` | ({`commonDirectory`}/libraries/{`path` OR resolved}) |
| `Library` | ({`commonDirectory`}/libraries/{`path` OR resolved}) |
| `ForgeMod` | ({`commonDirectory`}/modstore/{`path` OR resolved}) |
| `LiteMod` | ({`commonDirectory`}/modstore/{`path` OR resolved}) |
| `FabricMod` | ({`commonDirectory`}/mods/fabric/{`path` OR resolved}) |
| `File` | ({`instanceDirectory`}/{`Server.id`}/{`path` OR resolved}) |
The `commonDirectory` and `instanceDirectory` values are stored in the launcher's config.json.
### `Artifact.size: number`
The size of the artifact.
### `Artifact.MD5: string`
The MD5 hash of the artifact. This will be used to validate local artifacts.
### `Artifact.path: string`
**OPTIONAL**
A relative path to where the file will be saved. This is appended to the base path for the module's declared type.
If this is not specified, the path will be resolved based on the module's ID.
### `Artifact.url: string/url`
The artifact's download url.
## Required Object
### `Required.value: boolean`
**OPTIONAL**
If the module is required. Defaults to true if this property is omited.
### `Required.def: boolean`
**OPTIONAL**
If the module is enabled by default. Has no effect unless `Required.value` is false. Defaults to true if this property is omited.
---
## Module Types
### ForgeHosted
The module type `ForgeHosted` represents forge itself. Currently, the launcher only supports modded servers, as vanilla servers can be connected to via the mojang launcher. The `Hosted` part is key, this means that the forge module must declare its required libraries as submodules.
Ex.
```json
{
"id": "net.minecraftforge:forge:1.11.2-13.20.1.2429",
"name": "Minecraft Forge 1.11.2-13.20.1.2429",
"type": "ForgeHosted",
"artifact": {
"size": 4450992,
"MD5": "3fcc9b0104f0261397d3cc897e55a1c5",
"url": "http://files.minecraftforge.net/maven/net/minecraftforge/forge/1.11.2-13.20.1.2429/forge-1.11.2-13.20.1.2429-universal.jar"
},
"subModules": [
{
"id": "net.minecraft:launchwrapper:1.12",
"name": "Mojang (LaunchWrapper)",
"type": "Library",
"artifact": {
"size": 32999,
"MD5": "934b2d91c7c5be4a49577c9e6b40e8da",
"url": "http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/launchwrapper-1.12.jar"
}
}
]
}
```
All of forge's required libraries are declared in the `version.json` file found in the root of the forge jar file. These libraries MUST be hosted and declared a submodules or forge will not work.
There were plans to add a `Forge` type, in which the required libraries would be resolved by the launcher and downloaded from forge's servers. The forge servers are down at times, however, so this plan was stopped half-implemented.
---
### Fabric
The module type `Fabric` represents the fabric mod loader. Currently, the launcher only supports modded servers, as vanilla servers can be connected to via the mojang launcher.
Ex.
```json
{
"id": "net.fabricmc:fabric-loader:0.15.0",
"name": "Fabric (fabric-loader)",
"type": "Fabric",
"artifact": {
"size": 1196222,
"MD5": "a43d5a142246801343b6cedef1c102c4",
"url": "http://localhost:8080/repo/lib/net/fabricmc/fabric-loader/0.15.0/fabric-loader-0.15.0.jar"
},
"subModules": [
{
"id": "1.20.1-fabric-0.15.0",
"name": "Fabric (version.json)",
"type": "VersionManifest",
"artifact": {
"size": 2847,
"MD5": "69a2bd43452325ba1bc882fa0904e054",
"url": "http://localhost:8080/repo/versions/1.20.1-fabric-0.15.0/1.20.1-fabric-0.15.0.json"
}
}
}
```
Fabric works similarly to Forge 1.13+.
---
### LiteLoader
The module type `LiteLoader` represents liteloader. It is handled as a library and added to the classpath at runtime. Special launch conditions are executed when liteloader is present and enabled. This module can be optional and toggled similarly to `ForgeMod` and `Litemod` modules.
Ex.
```json
{
"id": "com.mumfrey:liteloader:1.11.2",
"name": "Liteloader (1.11.2)",
"type": "LiteLoader",
"required": {
"value": false,
"def": false
},
"artifact": {
"size": 1685422,
"MD5": "3a98b5ed95810bf164e71c1a53be568d",
"url": "http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/liteloader-1.11.2.jar"
},
"subModules": [
"All LiteMods go here"
]
}
```
---
### Library
The module type `Library` represents a library file which will be required to start the minecraft process. Each library module will be dynamically added to the `-cp` (classpath) argument while building the game process.
Ex.
```json
{
"id": "net.sf.jopt-simple:jopt-simple:4.6",
"name": "Jopt-simple 4.6",
"type": "Library",
"artifact": {
"size": 62477,
"MD5": "13560a58a79b46b82057686543e8d727",
"url": "http://mc.westeroscraft.com/WesterosCraftLauncher/files/1.11.2/jopt-simple-4.6.jar"
}
}
```
---
### ForgeMod
The module type `ForgeMod` represents a mod loaded by the Forge Mod Loader (FML). These files are stored maven-style and passed to FML using forge's [Modlist format](https://github.com/MinecraftForge/FML/wiki/New-JSON-Modlist-format).
Ex.
```json
{
"id": "com.westeroscraft:westerosblocks:3.0.0-beta-6-133",
"name": "WesterosBlocks (3.0.0-beta-6-133)",
"type": "ForgeMod",
"artifact": {
"size": 16321712,
"MD5": "5a89e2ab18916c18965fc93a0766cc6e",
"url": "http://mc.westeroscraft.com/WesterosCraftLauncher/prod-1.11.2/mods/WesterosBlocks.jar"
}
}
```
---
### LiteMod
The module type `LiteMod` represents a mod loaded by liteloader. These files are stored maven-style and passed to liteloader using forge's [Modlist format](https://github.com/MinecraftForge/FML/wiki/New-JSON-Modlist-format). Documentation for liteloader's implementation of this can be found on [this issue](http://develop.liteloader.com/liteloader/LiteLoader/issues/34).
Ex.
```json
{
"id": "com.mumfrey:macrokeybindmod:0.14.4-1.11.2@litemod",
"name": "Macro/Keybind Mod (0.14.4-1.11.2)",
"type": "LiteMod",
"required": {
"value": false,
"def": false
},
"artifact": {
"size": 1670811,
"MD5": "16080785577b391d426c62c8d3138558",
"url": "http://mc.westeroscraft.com/WesterosCraftLauncher/prod-1.11.2/mods/macrokeybindmod.litemod"
}
}
```
---
### File
The module type `file` represents a generic file required by the client, another module, etc. These files are stored in the server's instance directory.
Ex.
```json
{
"id": "com.westeroscraft:westeroscraftrp:2017-08-16",
"name": "WesterosCraft Resource Pack (2017-08-16)",
"type": "file",
"artifact": {
"size": 45241339,
"MD5": "ec2d9fdb14d5c2eafe5975a240202f1a",
"path": "resourcepacks/WesterosCraft.zip",
"url": "http://mc.westeroscraft.com/WesterosCraftLauncher/prod-1.11.2/resourcepacks/WesterosCraft.zip"
}
}
```

104
docs/launcher-catalog.md Normal file
View File

@@ -0,0 +1,104 @@
# Launcher Catalog
프로필은 관리자 측에서 미리 등록합니다.
- 기본 로컬 파일: `app/assets/launcher/catalog.json`
- 또는 운영용 원격 JSON
클라이언트는 설치 페이지에서 이 카탈로그를 읽기 전용으로 확인하고, 원하는 항목만 자기 라이브러리에 추가합니다.
## 형식
```json
{
"version": 1,
"profiles": [
{
"id": "my-modpack",
"name": "My Modpack",
"kind": "modpack",
"description": "설명",
"details": "설치 페이지 상세 패널에 표시할 긴 설명",
"distributionUrl": "https://example.com/launcher/distribution.json",
"defaultServerAddress": "example.com:25565",
"allowCustomServerAddress": true
},
{
"id": "my-map",
"name": "My Map",
"kind": "map",
"description": "싱글플레이 월드",
"details": "월드와 플레이 방식에 대한 상세 설명",
"distributionUrl": "https://example.com/launcher/vanilla-map-distribution.json",
"worldArchiveUrl": "https://example.com/worlds/my-map.zip",
"worldDirectoryName": "My Map",
"allowCustomServerAddress": false
},
{
"id": "my-server-pack",
"name": "My Server Pack",
"kind": "server-pack",
"description": "클라이언트 + 로컬 서버 번들",
"details": "서버 실행 방법, 권장 인원, 접속 방식 등 상세 설명",
"distributionUrl": "https://example.com/launcher/server-pack-client-distribution.json",
"serverBundleUrl": "https://example.com/serverpacks/my-server-pack.zip",
"serverDirectoryName": "my-server-pack",
"serverLaunchCommand": "java -jar server.jar nogui",
"serverPort": 25565,
"tunnelCommand": "playit-cli --port ${port}",
"tunnelAddressRegex": "([a-zA-Z0-9.-]+:\\d+)",
"allowCustomServerAddress": true
}
]
}
```
## 필드
- `id`: 내부 식별자
- `name`: 라이브러리/설치 페이지 표시 이름
- `kind`: `modpack`, `map`, `server-pack`
- `description`: 표시 설명
- `details`: 설치 페이지 상세 패널에 표시할 긴 설명
- `distributionUrl`: Helios distribution.json URL 또는 로컬 경로
- `defaultServerAddress`: 기본 자동 접속 주소
- `allowCustomServerAddress`: 사용자가 라이브러리에서 주소를 덮어쓸 수 있는지 여부
- `worldArchiveUrl`: `kind: map` 일 때 사용할 월드 ZIP 또는 로컬 경로
- `worldDirectoryName`: 게임 `saves/` 아래에 설치될 월드 폴더 이름
- `serverBundleUrl`: `kind: server-pack` 일 때 사용할 서버 ZIP 또는 로컬 디렉터리/경로
- `serverDirectoryName`: 서버 번들이 풀릴 하위 디렉터리 이름
- `serverLaunchCommand`: 로컬 서버 실행 명령. 비워두면 `start.sh`, `start.bat`, `server.jar` 순으로 추론
- `serverWorkingDirectory`: 실제 실행할 작업 디렉터리. 서버 루트 기준 상대 경로
- `serverPort`: 로컬 서버 포트
- `tunnelCommand`: 선택형 터널 명령. `${port}`, `${serverDir}` 치환 가능
- `tunnelAddressRegex`: 터널 stdout 에서 공개 주소를 추출할 정규식
## 런처가 계산하는 상태
아래 값들은 런처가 내부적으로 계산하는 상태라 파일에 직접 넣지 않아도 됩니다.
- `launchReady`: 실행에 필요한 필드가 모두 있는지 여부
- `hostReady`: `server-pack` 이 로컬 호스팅 가능한지 여부
판정 기준:
- `modpack`: `distributionUrl` 필요
- `map`: `distributionUrl`, `worldArchiveUrl`, `worldDirectoryName` 필요
- `server-pack`: 클라이언트 실행은 `distributionUrl`, 로컬 호스팅은 추가로 `serverBundleUrl` 필요
## 현재 구현 범위
- `modpack`: 지원
- `map`: 월드 ZIP 사전 다운로드, `saves/` 설치, `--quickPlaySingleplayer` 실행 지원
- `server-pack`: distribution 기반 클라이언트 + 수동 주소 입력 자동 접속 + 로컬 서버 실행 + 선택형 터널 명령 지원
## 네트워크 참고
포트포워딩 없이 외부 사용자가 접속하려면 런처 단독으로는 부족합니다.
필요한 것 중 하나:
- 별도 중계 서버
- 터널링 도구
- VPN/NAT traversal 백엔드
현재 구현은 그 중 `터널 명령 실행기`를 연결할 수 있는 자리까지만 제공합니다.

View File

@@ -0,0 +1,264 @@
# Portforwarding-Free Connection Plan
이 문서는 “포트포워딩 없이 런처만으로 접속” 기능을 나중에 구현하기 위한 설계 초안이다. 현재는 구현하지 않았고, `server-pack` 프로필의 `tunnelCommand` 자리에 외부 도구를 연결할 수 있는 수준까지만 준비되어 있다.
## 목표
- 호스트 사용자가 공유기 포트포워딩을 직접 설정하지 않아도 됨
- 접속 사용자는 런처에서 세션 주소 또는 세션 코드를 받아 바로 접속 가능
- 가능하면 버튼 몇 번으로 `서버 실행 -> 세션 공개 -> 다른 사용자 접속` 흐름이 끝나야 함
- 관리자가 런처와 백엔드를 함께 운영할 수 있어야 함
## 원하는 사용자 흐름
### 호스트
1. 라이브러리에서 `server-pack` 프로필 선택
2. `서버 실행` 클릭
3. 런처가 서버 프로세스를 시작
4. 런처가 백엔드에 세션 생성 요청
5. 백엔드가 공개 접속 주소 또는 세션 코드를 발급
6. 호스트는 그 주소/코드를 다른 사용자에게 전달
### 접속자
1. 같은 프로필을 라이브러리에 추가
2. 호스트가 보낸 주소 또는 세션 코드를 입력
3. 런처가 실제 접속 주소를 해석
4. 마인크래프트를 자동 실행하고 해당 서버로 바로 접속
## 필요한 구성요소
### 1. Session API
역할:
- 세션 생성
- 세션 갱신
- 세션 종료
- 세션 조회
- 세션 코드와 실제 릴레이 주소 매핑
예상 엔드포인트:
- `POST /api/sessions`
- `PATCH /api/sessions/:id`
- `DELETE /api/sessions/:id`
- `GET /api/sessions/:code`
세션 데이터 예시:
```json
{
"id": "sess_123",
"profileId": "my-server-pack",
"hostUserId": "uuid-or-account-id",
"relayAddress": "relay.example.com:34192",
"accessCode": "ABCD-EFGH",
"status": "active",
"createdAt": "2026-05-04T10:00:00Z",
"lastHeartbeatAt": "2026-05-04T10:01:00Z"
}
```
### 2. Relay or Tunnel Layer
실제로 포트포워딩 없는 접속을 가능하게 하는 핵심이다.
선택지:
- 자체 Relay 서버 운영
- 기존 터널링 솔루션 연동
- TURN 비슷한 중계 방식
- 전용 게임 프록시 운영
이 프로젝트에서는 우선 아래 두 단계 접근이 현실적이다.
1. 외부 터널링 도구 연동
2. 자체 세션/중계 서버로 점진적 전환
### 3. Launcher Host Agent
호스트 측 런처가 해야 하는 일:
- 로컬 서버 실행 상태 확인
- 백엔드에 세션 생성
- 터널 또는 릴레이 프로세스 시작
- 공개 주소 수신
- 주기적 heartbeat 전송
- 서버 종료 시 세션 종료
### 4. Launcher Join Resolver
접속자 측 런처가 해야 하는 일:
- 세션 코드 입력 UI 제공
- 코드로 세션 조회
- 실제 접속 주소 해석
- 선택 프로필과 세션 프로필 일치 여부 확인
- 게임 실행 시 자동 접속 파라미터 전달
## 권장 1차 구현 방향
가장 빠른 방향은 “전용 Relay”를 바로 만드는 것이 아니라, “Session API + 터널 도구 연동”부터 시작하는 것이다.
### 이유
- 현재 런처에 `server-pack`, `tunnelCommand`, 공개 주소 표시 흐름이 이미 들어가 있음
- 가장 적은 변경으로 실사용 가능 여부를 먼저 검증할 수 있음
- 이후 자체 릴레이로 갈아탈 때 UI와 라이브러리 모델을 그대로 유지할 수 있음
### 1차 구성
- 런처:
- 서버 실행
- 터널 명령 실행
- 터널 출력에서 공개 주소 파싱
- Session API에 주소 등록
- 백엔드:
- 세션 코드 발급
- 코드 -> 주소 매핑
- heartbeat 만료 처리
- 접속자 런처:
- 세션 코드 입력
- 코드 조회
- 자동 접속
## 이후 2차 구현 방향
터널 명령에 외부 도구를 직접 의존하지 않고, 전용 서비스로 이동한다.
### 추가되는 것
- 릴레이 노드
- 호스트 인증 토큰
- 세션별 임시 접속 권한
- 트래픽/세션 제한
- 관리자 대시보드
### 기대 효과
- 터널 도구 설치 부담 감소
- 사용자 경험 단순화
- 접속 로그와 세션 통계 수집 가능
- 악용 방지 정책 적용 가능
## 런처 쪽에 나중에 추가할 UI
### 라이브러리
- `세션 만들기`
- `세션 코드 복사`
- `세션 종료`
- `세션 상태 보기`
### 설치/실행
- `세션 코드로 접속`
- 최근 접속 세션 기록
- 세션 만료 안내
### 설정
- 세션 API 주소
- 릴레이 지역 선택
- 호스트 세션 자동 재등록 여부
## 프로필 스키마 확장 예정
나중에 `catalog.json` 또는 분리된 관리자 설정에 아래 필드가 추가될 수 있다.
```json
{
"sessionBackendUrl": "https://session.example.com",
"sessionJoinMode": "code",
"sessionCodeLength": 8,
"relayMode": "external-tunnel",
"relayRegion": "ap-northeast-2"
}
```
## 보안 고려사항
필수 항목:
- 세션 생성은 인증된 사용자만 가능해야 함
- 호스트 heartbeat가 끊기면 세션 자동 만료
- 세션 코드는 충분히 예측 불가능해야 함
- 동일 계정의 세션 수 제한 필요
- 악의적 트래픽에 대한 rate limit 필요
- 프로필 ID 불일치 세션 차단 또는 경고 필요
추가 고려:
- 세션 코드에 만료 시간 포함
- 백엔드 서명 토큰 사용
- 서버 로그 업로드 여부
- 접속자 IP/세션 기록 보관 정책
## 장애 대응
### 호스트 측
- 서버는 켜졌는데 릴레이 주소 발급 실패
- 터널은 살아있는데 heartbeat 실패
- 서버 프로세스 종료 후 세션 정리 누락
대응:
- 런처가 재시도 정책 보유
- 종료 이벤트에서 세션 삭제 호출
- stale session 정리 배치 작업 필요
### 접속자 측
- 세션 코드는 맞는데 만료됨
- 주소는 받았는데 릴레이가 이미 죽음
- 잘못된 프로필로 접속 시도
대응:
- 사용자에게 구체적인 실패 메시지 제공
- 세션 재조회 버튼
- 프로필 불일치 시 설치 페이지 이동 유도
## 구현 순서 제안
### 1단계
- Session API 설계
- 세션 코드 발급/조회/만료 로직 구현
- 런처에 세션 코드 입력 UI 추가
### 2단계
- 현재 `tunnelCommand` 기반 흐름과 Session API 연결
- 호스트 세션 등록/heartbeat/종료 처리
- 접속자 자동 접속 완료
### 3단계
- 자체 Relay 서비스 검토
- 전용 호스트 인증 및 운영 정책 도입
- 외부 터널 도구 의존도 축소
## 현재 코드와 연결되는 지점
- `app/assets/js/serverruntime.js`
- 서버 실행 후 공개 주소 확보 지점
- `app/assets/js/scripts/library.js`
- 호스트 공개 주소 표시 및 수동 주소 입력 UI
- `app/assets/js/processbuilder.js`
- 자동 접속 인자 전달 지점
- `app/assets/js/catalogmanager.js`
- 프로필 메타데이터 확장 지점
## 결론
“포트포워딩 없이 접속”은 런처 버튼 하나로 끝나는 기능처럼 보여도, 실제로는 런처만의 문제가 아니라 세션 백엔드와 릴레이 계층이 함께 있어야 성립한다.
따라서 다음 구현 목표는 아래가 적절하다.
1. `Session API` 설계
2. `server-pack + tunnelCommand` 흐름을 세션 코드 모델과 연결
3. 이후 전용 Relay 서비스로 확장

File diff suppressed because it is too large Load Diff