├── .github
└── workflows
│ ├── build.yml
│ └── release.yml
├── .gitignore
├── .vscode
├── launch.json
└── tasks.json
├── LICENSE
├── README.md
├── ShadowsocksUriGenerator.CLI.Tests
└── ShadowsocksUriGenerator.CLI.Tests.csproj
├── ShadowsocksUriGenerator.CLI.Utils
├── ConsoleHelper.cs
├── ReportHelper.cs
└── ShadowsocksUriGenerator.CLI.Utils.csproj
├── ShadowsocksUriGenerator.CLI
├── GroupCommand.cs
├── NodeCommand.cs
├── OnlineConfigCommand.cs
├── OutlineServerCommand.cs
├── Parsers.cs
├── Program.cs
├── Properties
│ └── launchSettings.json
├── ReportCommand.cs
├── ServiceCommand.cs
├── SettingsCommand.cs
├── ShadowsocksUriGenerator.CLI.csproj
├── UserCommand.cs
├── Validators.cs
└── ss-uri-gen.ico
├── ShadowsocksUriGenerator.Chatbot.Telegram.Tests
└── ShadowsocksUriGenerator.Chatbot.Telegram.Tests.csproj
├── ShadowsocksUriGenerator.Chatbot.Telegram
├── BotConfig.cs
├── BotConfigJsonSerializerContext.cs
├── CLI
│ ├── BotRunner.cs
│ └── ConfigCommand.cs
├── Commands
│ ├── AuthCommands.cs
│ ├── CredCommands.cs
│ ├── DataCommands.cs
│ ├── ListCommands.cs
│ └── ReportCommand.cs
├── Program.cs
├── ShadowsocksUriGenerator.Chatbot.Telegram.csproj
├── UpdateHandler.cs
├── Utils
│ ├── ChatHelper.cs
│ └── DataHelper.cs
├── ss-uri-gen-chatbot-telegram.ico
└── ss-uri-gen-chatbot-telegram.png
├── ShadowsocksUriGenerator.Rescue.CLI
├── Program.cs
├── ShadowsocksUriGenerator.Rescue.CLI.csproj
└── ss-uri-gen-rescue.ico
├── ShadowsocksUriGenerator.Rescue
├── Rescuers.cs
├── Restorers.cs
├── ShadowsocksUriGenerator.Rescue.csproj
└── ss-uri-gen-rescue.png
├── ShadowsocksUriGenerator.Server
├── Controllers
│ ├── OOCv1Controller.cs
│ ├── OnlineConfigControllerBase.cs
│ ├── SIP008Controller.cs
│ ├── ShadowsocksGoClientConfigController.cs
│ └── SingBoxOutboundConfigController.cs
├── Program.cs
├── Properties
│ └── launchSettings.json
├── ShadowsocksUriGenerator.Server.csproj
├── Utils
│ ├── FilterHelper.cs
│ ├── HeaderHelper.cs
│ └── JsonHelper.cs
├── appsettings.Development.json
├── appsettings.json
└── appsettings.systemd.json
├── ShadowsocksUriGenerator.Services
├── DataService.cs
├── IDataService.cs
└── ShadowsocksUriGenerator.Services.csproj
├── ShadowsocksUriGenerator.Tests
├── NodesTests.cs
├── OnlineConfigTests.cs
├── ShadowsocksUriGenerator.Tests.csproj
├── UriTests.cs
├── UsersTests.cs
└── UtilTests.cs
├── ShadowsocksUriGenerator
├── Data
│ ├── DataJsonSerializerContext.cs
│ ├── Group.cs
│ ├── GroupApiRequestException.cs
│ ├── MemberInfo.cs
│ ├── Node.cs
│ ├── Nodes.cs
│ ├── SSMv1Server.cs
│ ├── User.cs
│ └── Users.cs
├── Federation
│ ├── Config
│ │ ├── FederatedPeerConfig.cs
│ │ ├── FederationConfig.cs
│ │ ├── HostUserConfig.cs
│ │ └── Shadowsocks
│ │ │ ├── HostGroupConfigOutline.cs
│ │ │ ├── HostGroupConfigShadowsocks.cs
│ │ │ ├── HostGroupConfigShadowsocksManager.cs
│ │ │ └── HostGroupConfigShadowsocksSimple.cs
│ ├── Data
│ │ ├── DataUsage.cs
│ │ ├── FederatedPeerData.cs
│ │ ├── FederationData.cs
│ │ ├── HostUserData.cs
│ │ ├── PeerUserData.cs
│ │ └── Shadowsocks
│ │ │ ├── HostGroupDataOutline.cs
│ │ │ ├── HostGroupDataShadowsocks.cs
│ │ │ └── HostGroupDataShadowsocksManager.cs
│ └── Protocols
│ │ └── Shadowsocks
│ │ ├── FederatedShadowsocksServerConfig.cs
│ │ └── FederatedShadowsocksServerCredential.cs
├── Manager
│ ├── IManagerApiClient.cs
│ ├── IManagerApiTransport.cs
│ ├── ManagerAPIJsonSerializerContext.cs
│ ├── ManagerApiClient.cs
│ ├── ManagerApiRequest.cs
│ ├── ManagerApiResponse.cs
│ └── ManagerApiTransport.cs
├── OnlineConfig
│ ├── DateTimeOffsetUnixTimeSecondsConverter.cs
│ ├── OOCv1ApiToken.cs
│ ├── OOCv1ConfigBase.cs
│ ├── OOCv1ShadowsocksConfig.cs
│ ├── OOCv1ShadowsocksServer.cs
│ ├── OnlineConfigJsonSerializerContext.cs
│ ├── SIP008Config.cs
│ ├── SIP008Server.cs
│ ├── SIP008StaticGen.cs
│ ├── ShadowsocksGoClientConfig.cs
│ ├── ShadowsocksGoConfig.cs
│ ├── SingBoxConfig.cs
│ ├── SingBoxMultiplexConfig.cs
│ └── SingBoxOutboundConfig.cs
├── Outline
│ ├── DateTimeOffsetUnixTimeMillisecondsConverter.cs
│ ├── OutlineAccessKey.cs
│ ├── OutlineApiClient.cs
│ ├── OutlineJsonSerializerContext.cs
│ ├── OutlineModels.cs
│ └── OutlineServerInfo.cs
├── Protocols
│ └── Shadowsocks
│ │ └── ShadowsocksServerConfig.cs
├── SSMv1
│ ├── SSMv1ApiClient.cs
│ ├── SSMv1ApiException.cs
│ ├── SSMv1Error.cs
│ ├── SSMv1JsonSerializerContext.cs
│ ├── SSMv1ServerInfo.cs
│ ├── SSMv1Stats.cs
│ └── SSMv1User.cs
├── Settings.cs
├── SettingsJsonSerializerContext.cs
├── ShadowsocksUriGenerator.csproj
├── SortBy.cs
├── Utils
│ ├── FileHelper.cs
│ └── InteractionHelper.cs
└── ss-uri-gen.png
├── docs
├── README.md
└── reverse-proxy-guide.md
├── nuget.config
├── renovate.json
├── shadowsocks-uri-generator.sln
└── systemd
└── user
├── ss-uri-gen-chatbot-telegram.service
├── ss-uri-gen-server.service
└── ss-uri-gen.service
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.2.0",
3 | "configurations": [
4 | {
5 | "name": "Launch ss-uri-gen in Interactive Mode",
6 | "type": "coreclr",
7 | "request": "launch",
8 | "preLaunchTask": "Build (Debug)",
9 | "program": "${workspaceFolder}/ShadowsocksUriGenerator/bin/Debug/net9.0/ss-uri-gen.dll",
10 | "args": [
11 | "interactive"
12 | ],
13 | "cwd": "${workspaceFolder}/ShadowsocksUriGenerator/bin/Debug/net9.0",
14 | "stopAtEntry": false,
15 | "console": "integratedTerminal"
16 | },
17 | {
18 | "name": ".NET Attach",
19 | "type": "coreclr",
20 | "request": "attach",
21 | "processId": "${command:pickProcess}"
22 | }
23 | ]
24 | }
25 |
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "2.0.0",
3 | "tasks": [
4 | {
5 | "label": "Build (Debug)",
6 | "command": "dotnet",
7 | "type": "process",
8 | "args": [
9 | "build",
10 | "/property:GenerateFullPaths=true",
11 | "/consoleloggerparameters:NoSummary"
12 | ],
13 | "problemMatcher": "$msCompile",
14 | "group": {
15 | "kind": "build",
16 | "isDefault": true
17 | }
18 | },
19 | {
20 | "label": "Build (Release)",
21 | "command": "dotnet",
22 | "type": "process",
23 | "args": [
24 | "build",
25 | "-c",
26 | "Release",
27 | "/property:GenerateFullPaths=true",
28 | "/consoleloggerparameters:NoSummary"
29 | ],
30 | "problemMatcher": "$msCompile"
31 | },
32 | {
33 | "label": "Build ss-uri-gen (Debug)",
34 | "command": "dotnet",
35 | "type": "process",
36 | "args": [
37 | "build",
38 | "${workspaceFolder}/ShadowsocksUriGenerator/ShadowsocksUriGenerator.csproj",
39 | "/property:GenerateFullPaths=true",
40 | "/consoleloggerparameters:NoSummary"
41 | ],
42 | "problemMatcher": "$msCompile"
43 | },
44 | {
45 | "label": "Build ss-uri-gen (Release)",
46 | "command": "dotnet",
47 | "type": "process",
48 | "args": [
49 | "build",
50 | "${workspaceFolder}/ShadowsocksUriGenerator/ShadowsocksUriGenerator.csproj",
51 | "-c",
52 | "Release",
53 | "/property:GenerateFullPaths=true",
54 | "/consoleloggerparameters:NoSummary"
55 | ],
56 | "problemMatcher": "$msCompile"
57 | },
58 | {
59 | "label": "Build ss-uri-gen-chatbot-telegram (Debug)",
60 | "command": "dotnet",
61 | "type": "process",
62 | "args": [
63 | "build",
64 | "${workspaceFolder}/ShadowsocksUriGenerator.Chatbot.Telegram/ShadowsocksUriGenerator.Chatbot.Telegram.csproj",
65 | "/property:GenerateFullPaths=true",
66 | "/consoleloggerparameters:NoSummary"
67 | ],
68 | "problemMatcher": "$msCompile"
69 | },
70 | {
71 | "label": "Build ss-uri-gen-chatbot-telegram (Debug)",
72 | "command": "dotnet",
73 | "type": "process",
74 | "args": [
75 | "build",
76 | "${workspaceFolder}/ShadowsocksUriGenerator.Chatbot.Telegram/ShadowsocksUriGenerator.Chatbot.Telegram.csproj",
77 | "-c",
78 | "Release",
79 | "/property:GenerateFullPaths=true",
80 | "/consoleloggerparameters:NoSummary"
81 | ],
82 | "problemMatcher": "$msCompile"
83 | },
84 | {
85 | "label": "Publish ss-uri-gen as fxdependent",
86 | "command": "dotnet",
87 | "type": "process",
88 | "args": [
89 | "publish",
90 | "${workspaceFolder}/ShadowsocksUriGenerator/ShadowsocksUriGenerator.csproj",
91 | "-c",
92 | "Release",
93 | "/property:GenerateFullPaths=true",
94 | "/consoleloggerparameters:NoSummary"
95 | ],
96 | "problemMatcher": "$msCompile"
97 | },
98 | {
99 | "label": "Publish ss-uri-gen-chatbot-telegram as fxdependent",
100 | "command": "dotnet",
101 | "type": "process",
102 | "args": [
103 | "publish",
104 | "${workspaceFolder}/ShadowsocksUriGenerator.Chatbot.Telegram/ShadowsocksUriGenerator.Chatbot.Telegram.csproj",
105 | "-c",
106 | "Release",
107 | "/property:GenerateFullPaths=true",
108 | "/consoleloggerparameters:NoSummary"
109 | ],
110 | "problemMatcher": "$msCompile"
111 | },
112 | {
113 | "label": "Watch ss-uri-gen",
114 | "command": "dotnet",
115 | "type": "process",
116 | "args": [
117 | "watch",
118 | "run",
119 | "${workspaceFolder}/ShadowsocksUriGenerator/ShadowsocksUriGenerator.csproj",
120 | "/property:GenerateFullPaths=true",
121 | "/consoleloggerparameters:NoSummary"
122 | ],
123 | "problemMatcher": "$msCompile"
124 | }
125 | ]
126 | }
127 |
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator.CLI.Tests/ShadowsocksUriGenerator.CLI.Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net9.0
5 |
6 | false
7 |
8 | enable
9 | enable
10 |
11 |
12 |
13 |
14 |
15 |
16 | runtime; build; native; contentfiles; analyzers; buildtransitive
17 | all
18 |
19 |
20 | runtime; build; native; contentfiles; analyzers; buildtransitive
21 | all
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator.CLI.Utils/ConsoleHelper.cs:
--------------------------------------------------------------------------------
1 | using System.Text;
2 |
3 | namespace ShadowsocksUriGenerator.CLI.Utils
4 | {
5 | public static class ConsoleHelper
6 | {
7 | public static void PrintTableBorder(params int[] columnWidths)
8 | {
9 | foreach (var columnWidth in columnWidths)
10 | {
11 | Console.Write('+');
12 | for (var i = 0; i < columnWidth; i++)
13 | Console.Write('-');
14 | }
15 |
16 | Console.WriteLine("+");
17 | }
18 |
19 | public static StringBuilder AppendTableBorder(this StringBuilder stringBuilder, params int[] columnWidths)
20 | {
21 | foreach (var columnWidth in columnWidths)
22 | {
23 | stringBuilder.Append('+');
24 | for (var i = 0; i < columnWidth; i++)
25 | stringBuilder.Append('-');
26 | }
27 |
28 | stringBuilder.AppendLine("+");
29 |
30 | return stringBuilder;
31 | }
32 |
33 | public static void PrintNameList(IEnumerable names, bool onePerLine = false)
34 | {
35 | if (onePerLine)
36 | {
37 | foreach (var name in names)
38 | {
39 | Console.WriteLine(name);
40 | }
41 | }
42 | else
43 | {
44 | var stringBuilder = new StringBuilder();
45 |
46 | foreach (var name in names)
47 | {
48 | if (name.Contains(' '))
49 | stringBuilder.Append($"\"{name}\" ");
50 | else
51 | stringBuilder.Append($"{name} ");
52 | }
53 |
54 | if (names.Any())
55 | {
56 | stringBuilder.Remove(stringBuilder.Length - 1, 1); // remove trailing space
57 | stringBuilder.AppendLine();
58 | }
59 |
60 | var output = stringBuilder.ToString();
61 | Console.Write(output);
62 | }
63 | }
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator.CLI.Utils/ReportHelper.cs:
--------------------------------------------------------------------------------
1 | using System.Text;
2 |
3 | namespace ShadowsocksUriGenerator.CLI.Utils
4 | {
5 | public static class ReportHelper
6 | {
7 | public static (string dataUsageByGroup, string dataUsageByUser) GenerateDataUsageCSV(
8 | IEnumerable<(string group, ulong bytesUsed, ulong bytesRemaining)> recordsByGroup,
9 | IEnumerable<(string username, ulong bytesUsed, ulong bytesRemaining)> recordsByUser)
10 | {
11 | var groupSB = new StringBuilder();
12 | groupSB.Append("Group,Data Used,Data Remaining\r\n");
13 | foreach (var (group, bytesUsed, bytesRemaining) in recordsByGroup)
14 | {
15 | groupSB.Append(group);
16 | if (bytesUsed > 0UL)
17 | groupSB.Append($",{bytesUsed}");
18 | else
19 | groupSB.Append(',');
20 | if (bytesRemaining > 0UL)
21 | groupSB.Append($",{bytesRemaining}\r\n");
22 | else
23 | groupSB.Append(",\r\n");
24 | }
25 |
26 | var userSB = new StringBuilder();
27 | userSB.Append("User,Data Used,Data Remaining\r\n");
28 | foreach (var (username, bytesUsed, bytesRemaining) in recordsByUser)
29 | {
30 | userSB.Append(username);
31 | if (bytesUsed > 0UL)
32 | userSB.Append($",{bytesUsed}");
33 | else
34 | userSB.Append(',');
35 | if (bytesRemaining > 0UL)
36 | userSB.Append($",{bytesRemaining}\r\n");
37 | else
38 | userSB.Append(",\r\n");
39 | }
40 |
41 | return (groupSB.ToString(), userSB.ToString());
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator.CLI.Utils/ShadowsocksUriGenerator.CLI.Utils.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net9.0
5 | enable
6 | enable
7 | true
8 | 5.2.0
9 | database64128
10 | Utilities library for ShadowsocksUriGenerator.CLI
11 | © 2023 database64128
12 | LICENSE
13 | https://github.com/database64128/shadowsocks-uri-generator
14 | https://github.com/database64128/shadowsocks-uri-generator
15 | Public
16 | false
17 |
18 |
19 |
20 |
21 | True
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator.CLI/Parsers.cs:
--------------------------------------------------------------------------------
1 | using ShadowsocksUriGenerator.Utils;
2 | using System.CommandLine.Parsing;
3 |
4 | namespace ShadowsocksUriGenerator.CLI
5 | {
6 | ///
7 | /// Class for common utility parsers.
8 | /// Do not put command-specific parsers here.
9 | ///
10 | public static class Parsers
11 | {
12 | ///
13 | /// Parses the string representation of the port number
14 | /// into an integer representation.
15 | ///
16 | /// The argument result.
17 | /// The integer representation of the port number.
18 | public static int ParsePortNumber(ArgumentResult argumentResult)
19 | {
20 | var portString = argumentResult.Tokens.Single().Value;
21 | if (int.TryParse(portString, out var port))
22 | {
23 | if (port is > 0 and <= 65535)
24 | {
25 | return port;
26 | }
27 | else
28 | {
29 | argumentResult.AddError("Port out of range: (0, 65535]");
30 | return default;
31 | }
32 | }
33 | else
34 | {
35 | argumentResult.AddError($"Invalid port number: {portString}");
36 | return default;
37 | }
38 | }
39 |
40 | ///
41 | /// Parses the Shadowsocks AEAD method string
42 | /// into the unified form.
43 | ///
44 | /// The argument result.
45 | /// The Shadowsocks AEAD method string in the unified form.
46 | public static string ParseShadowsocksAEADMethod(ArgumentResult argumentResult)
47 | {
48 | var method = argumentResult.Tokens.Single().Value;
49 |
50 | switch (method)
51 | {
52 | case "AEAD_AES_128_GCM" or "aes-128-gcm":
53 | return "aes-128-gcm";
54 | case "AEAD_AES_256_GCM" or "aes-256-gcm":
55 | return "aes-256-gcm";
56 | case "AEAD_CHACHA20_POLY1305" or "chacha20-poly1305" or "chacha20-ietf-poly1305":
57 | return "chacha20-ietf-poly1305";
58 | case "2022-blake3-aes-128-gcm" or "2022-blake3-aes-256-gcm" or "2022-blake3-chacha8-poly1305" or "2022-blake3-chacha12-poly1305" or "2022-blake3-chacha20-poly1305":
59 | return method;
60 | default:
61 | argumentResult.AddError($"Invalid Shadowsocks AEAD or 2022 method: {method}");
62 | return string.Empty;
63 | }
64 | }
65 |
66 | ///
67 | /// Parses the Shadowsocks 2022 method string.
68 | ///
69 | /// The argument result.
70 | /// The Shadowsocks 2022 method string.
71 | public static string ParseShadowsocks2022Method(ArgumentResult argumentResult)
72 | {
73 | var method = argumentResult.Tokens.Single().Value;
74 | switch (method)
75 | {
76 | case "2022-blake3-aes-128-gcm" or "2022-blake3-aes-256-gcm" or "2022-blake3-chacha8-poly1305" or "2022-blake3-chacha12-poly1305" or "2022-blake3-chacha20-poly1305":
77 | return method;
78 | default:
79 | argumentResult.AddError($"Invalid Shadowsocks 2022 method: {method}");
80 | return string.Empty;
81 | }
82 | }
83 |
84 | ///
85 | /// Parses the string representation of an amount of data
86 | /// into the number of bytes it represents.
87 | ///
88 | /// The argument result.
89 | /// The number of bytes. Null if not specified.
90 | public static ulong? ParseDataString(ArgumentResult argumentResult)
91 | {
92 | var dataString = argumentResult.Tokens.Single().Value;
93 |
94 | // Not specified
95 | if (string.IsNullOrEmpty(dataString))
96 | {
97 | return null;
98 | }
99 |
100 | if (InteractionHelper.TryParseDataLimitString(dataString, out var dataLimitInBytes))
101 | {
102 | return dataLimitInBytes;
103 | }
104 | else
105 | {
106 | argumentResult.AddError($"Invalid data string representation: {dataString}");
107 | return default;
108 | }
109 | }
110 |
111 | ///
112 | /// Parses the string representation of an absolute URI
113 | /// into a Uri object.
114 | ///
115 | /// The argument result.
116 | /// The Uri object.
117 | public static Uri? ParseAbsoluteUri(ArgumentResult argumentResult)
118 | {
119 | var uriString = argumentResult.Tokens.Single().Value;
120 | if (Uri.TryCreate(uriString, UriKind.Absolute, out var uri))
121 | {
122 | return uri;
123 | }
124 | else
125 | {
126 | argumentResult.AddError($"Invalid URI: {uriString}");
127 | return default;
128 | }
129 | }
130 | }
131 | }
132 |
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator.CLI/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "profiles": {
3 | "ShadowsocksUriGenerator.CLI": {
4 | "commandName": "Project",
5 | "commandLineArgs": "interactive"
6 | }
7 | }
8 | }
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator.CLI/ServiceCommand.cs:
--------------------------------------------------------------------------------
1 | using ShadowsocksUriGenerator.Data;
2 | using ShadowsocksUriGenerator.OnlineConfig;
3 |
4 | namespace ShadowsocksUriGenerator.CLI;
5 |
6 | public static class ServiceCommand
7 | {
8 | public static async Task Run(CancellationToken cancellationToken = default)
9 | {
10 | while (true)
11 | {
12 | var (users, loadUsersErrMsg) = await Users.LoadUsersAsync(cancellationToken);
13 | if (loadUsersErrMsg is not null)
14 | {
15 | Console.WriteLine(loadUsersErrMsg);
16 | return 1;
17 | }
18 |
19 | var (loadedNodes, loadNodesErrMsg) = await Nodes.LoadNodesAsync(cancellationToken);
20 | if (loadNodesErrMsg is not null)
21 | {
22 | Console.WriteLine(loadNodesErrMsg);
23 | return 1;
24 | }
25 | using var nodes = loadedNodes;
26 |
27 | var (settings, loadSettingsErrMsg) = await Settings.LoadSettingsAsync(cancellationToken);
28 | if (loadSettingsErrMsg is not null)
29 | {
30 | Console.WriteLine(loadSettingsErrMsg);
31 | return 1;
32 | }
33 |
34 | if (settings.ServicePullFromServers)
35 | {
36 | try
37 | {
38 | await nodes.PullGroupsAsync(ReadOnlyMemory.Empty, users, settings, cancellationToken);
39 | }
40 | catch (Exception ex)
41 | {
42 | Console.WriteLine(ex);
43 | }
44 |
45 | Console.WriteLine("Pulled from servers.");
46 | }
47 | if (settings.ServiceDeployToServers)
48 | {
49 | try
50 | {
51 | await nodes.DeployGroupsAsync(ReadOnlyMemory.Empty, users, settings, cancellationToken);
52 | }
53 | catch (Exception ex)
54 | {
55 | Console.WriteLine(ex);
56 | }
57 |
58 | Console.WriteLine("Deployed to servers.");
59 | }
60 | if (settings.ServiceGenerateOnlineConfig)
61 | {
62 | var errMsg = await SIP008StaticGen.GenerateAndSave(users, nodes, settings, cancellationToken);
63 | if (errMsg is not null)
64 | Console.Write(errMsg);
65 |
66 | Console.WriteLine("Generated online config.");
67 | }
68 | if (settings.ServiceRegenerateOnlineConfig)
69 | {
70 | SIP008StaticGen.Remove(users, settings);
71 |
72 | Console.WriteLine("Cleaned online config.");
73 |
74 | var errMsg = await SIP008StaticGen.GenerateAndSave(users, nodes, settings, cancellationToken);
75 | if (errMsg is not null)
76 | Console.Write(errMsg);
77 |
78 | Console.WriteLine("Generated online config.");
79 | }
80 |
81 | var saveUsersErrMsg = await Users.SaveUsersAsync(users, cancellationToken);
82 | if (saveUsersErrMsg is not null)
83 | {
84 | Console.WriteLine(saveUsersErrMsg);
85 | return 1;
86 | }
87 |
88 | var saveNodesErrMsg = await Nodes.SaveNodesAsync(nodes, cancellationToken);
89 | if (saveNodesErrMsg is not null)
90 | {
91 | Console.WriteLine(saveNodesErrMsg);
92 | return 1;
93 | }
94 |
95 | try
96 | {
97 | await Task.Delay(settings.ServiceRunIntervalSecs * 1000, cancellationToken);
98 | }
99 | catch (TaskCanceledException)
100 | {
101 | return 0;
102 | }
103 | }
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator.CLI/SettingsCommand.cs:
--------------------------------------------------------------------------------
1 | using ShadowsocksUriGenerator.CLI.Utils;
2 |
3 | namespace ShadowsocksUriGenerator.CLI
4 | {
5 | public static class SettingsCommand
6 | {
7 | public static async Task Get(CancellationToken cancellationToken = default)
8 | {
9 | var (settings, loadSettingsErrMsg) = await Settings.LoadSettingsAsync(cancellationToken);
10 | if (loadSettingsErrMsg is not null)
11 | {
12 | Console.WriteLine(loadSettingsErrMsg);
13 | return 1;
14 | }
15 |
16 | ConsoleHelper.PrintTableBorder(42, 40);
17 | Console.WriteLine($"|{"Key",-42}|{"Value",40}|");
18 | ConsoleHelper.PrintTableBorder(42, 40);
19 |
20 | Console.WriteLine($"|{"Version",-42}|{settings.Version,40}|");
21 | Console.WriteLine($"|{"UserDataUsageDefaultSortBy",-42}|{settings.UserDataUsageDefaultSortBy,40}|");
22 | Console.WriteLine($"|{"GroupDataUsageDefaultSortBy",-42}|{settings.GroupDataUsageDefaultSortBy,40}|");
23 | Console.WriteLine($"|{"OnlineConfigSortByName",-42}|{settings.OnlineConfigSortByName,40}|");
24 | Console.WriteLine($"|{"OnlineConfigDeliverByGroup",-42}|{settings.OnlineConfigDeliverByGroup,40}|");
25 | Console.WriteLine($"|{"OnlineConfigCleanOnUserRemoval",-42}|{settings.OnlineConfigCleanOnUserRemoval,40}|");
26 | Console.WriteLine($"|{"OnlineConfigOutputDirectory",-42}|{settings.OnlineConfigOutputDirectory,40}|");
27 | Console.WriteLine($"|{"OnlineConfigDeliveryRootUri",-42}|{settings.OnlineConfigDeliveryRootUri,40}|");
28 | Console.WriteLine($"|{"OutlineServerApplyDefaultUserOnAssociation",-42}|{settings.OutlineServerApplyDefaultUserOnAssociation,40}|");
29 | Console.WriteLine($"|{"OutlineServerApplyDataLimitOnAssociationn",-42}|{settings.OutlineServerApplyDataLimitOnAssociation,40}|");
30 | Console.WriteLine($"|{"OutlineServerGlobalDefaultUser",-42}|{settings.OutlineServerGlobalDefaultUser,40}|");
31 | Console.WriteLine($"|{"ApiRequestConcurrency",-42}|{settings.ApiRequestConcurrency,40}|");
32 | Console.WriteLine($"|{"ApiServerBaseUrl",-42}|{settings.ApiServerBaseUrl,40}|");
33 | Console.WriteLine($"|{"ApiServerSecretPath",-42}|{settings.ApiServerSecretPath,40}|");
34 | Console.WriteLine($"|{"ServiceRunIntervalSecs",-42}|{settings.ServiceRunIntervalSecs,40}|");
35 | Console.WriteLine($"|{"ServicePullFromServers",-42}|{settings.ServicePullFromServers,40}|");
36 | Console.WriteLine($"|{"ServiceDeployToServers",-42}|{settings.ServiceDeployToServers,40}|");
37 | Console.WriteLine($"|{"ServiceGenerateOnlineConfig",-42}|{settings.ServiceGenerateOnlineConfig,40}|");
38 | Console.WriteLine($"|{"ServiceRegenerateOnlineConfig",-42}|{settings.ServiceRegenerateOnlineConfig,40}|");
39 |
40 | ConsoleHelper.PrintTableBorder(42, 40);
41 |
42 | return 0;
43 | }
44 |
45 | public static async Task Set(
46 | SortBy? userDataUsageDefaultSortBy,
47 | SortBy? groupDataUsageDefaultSortBy,
48 | bool? onlineConfigSortByName,
49 | bool? onlineConfigDeliverByGroup,
50 | bool? onlineConfigCleanOnUserRemoval,
51 | string? onlineConfigOutputDirectory,
52 | string? onlineConfigDeliveryRootUri,
53 | bool? outlineServerApplyDefaultUserOnAssociation,
54 | bool? outlineServerApplyDataLimitOnAssociation,
55 | string? outlineServerGlobalDefaultUser,
56 | int? apiRequestConcurrency,
57 | string? apiServerBaseUrl,
58 | string? apiServerSecretPath,
59 | int? serviceRunIntervalSecs,
60 | bool? servicePullFromServers,
61 | bool? serviceDeployToServers,
62 | bool? serviceGenerateOnlineConfig,
63 | bool? serviceRegenerateOnlineConfig,
64 | CancellationToken cancellationToken = default)
65 | {
66 | var (settings, loadSettingsErrMsg) = await Settings.LoadSettingsAsync(cancellationToken);
67 | if (loadSettingsErrMsg is not null)
68 | {
69 | Console.WriteLine(loadSettingsErrMsg);
70 | return 1;
71 | }
72 |
73 | if (userDataUsageDefaultSortBy is SortBy userSortBy)
74 | settings.UserDataUsageDefaultSortBy = userSortBy;
75 | if (groupDataUsageDefaultSortBy is SortBy groupSortBy)
76 | settings.GroupDataUsageDefaultSortBy = groupSortBy;
77 |
78 | if (onlineConfigSortByName is bool sortByName)
79 | settings.OnlineConfigSortByName = sortByName;
80 | if (onlineConfigDeliverByGroup is bool deliverByGroup)
81 | settings.OnlineConfigDeliverByGroup = deliverByGroup;
82 | if (onlineConfigCleanOnUserRemoval is bool cleanOnUserRemoval)
83 | settings.OnlineConfigCleanOnUserRemoval = cleanOnUserRemoval;
84 | if (!string.IsNullOrEmpty(onlineConfigOutputDirectory))
85 | settings.OnlineConfigOutputDirectory = onlineConfigOutputDirectory;
86 | if (!string.IsNullOrEmpty(onlineConfigDeliveryRootUri))
87 | settings.OnlineConfigDeliveryRootUri = onlineConfigDeliveryRootUri;
88 |
89 | if (outlineServerApplyDefaultUserOnAssociation is bool applyDefaultUserOnAssociation)
90 | settings.OutlineServerApplyDefaultUserOnAssociation = applyDefaultUserOnAssociation;
91 | if (outlineServerApplyDataLimitOnAssociation is bool applyDataLimitOnAssociation)
92 | settings.OutlineServerApplyDataLimitOnAssociation = applyDataLimitOnAssociation;
93 | if (!string.IsNullOrEmpty(outlineServerGlobalDefaultUser))
94 | settings.OutlineServerGlobalDefaultUser = outlineServerGlobalDefaultUser;
95 |
96 | if (apiRequestConcurrency is int concurrency)
97 | settings.ApiRequestConcurrency = concurrency;
98 |
99 | if (!string.IsNullOrEmpty(apiServerBaseUrl))
100 | settings.ApiServerBaseUrl = apiServerBaseUrl;
101 | if (!string.IsNullOrEmpty(apiServerSecretPath))
102 | settings.ApiServerSecretPath = apiServerSecretPath;
103 |
104 | if (serviceRunIntervalSecs is int intervalSecs)
105 | settings.ServiceRunIntervalSecs = intervalSecs;
106 | if (servicePullFromServers is bool pullFromServers)
107 | settings.ServicePullFromServers = pullFromServers;
108 | if (serviceDeployToServers is bool deployToServers)
109 | settings.ServiceDeployToServers = deployToServers;
110 | if (serviceGenerateOnlineConfig is bool generateOnlineConfig)
111 | settings.ServiceGenerateOnlineConfig = generateOnlineConfig;
112 | if (serviceRegenerateOnlineConfig is bool regenerateOnlineConfig)
113 | settings.ServiceRegenerateOnlineConfig = regenerateOnlineConfig;
114 |
115 | var saveSettingsErrMsg = await Settings.SaveSettingsAsync(settings, cancellationToken);
116 | if (saveSettingsErrMsg is not null)
117 | {
118 | Console.WriteLine(saveSettingsErrMsg);
119 | return 1;
120 | }
121 |
122 | return 0;
123 | }
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator.CLI/ShadowsocksUriGenerator.CLI.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | net9.0
6 | enable
7 | enable
8 | true
9 | ss-uri-gen
10 | ss-uri-gen.ico
11 | ShadowsocksUriGenerator.CLI
12 | 5.2.0
13 | database64128
14 | Shadowsocks URI Generator CLI
15 | Shadowsocks URI Generator command-line interface.
16 | © 2023 database64128
17 | LICENSE
18 | https://github.com/database64128/shadowsocks-uri-generator
19 | ss-uri-gen.png
20 | https://github.com/database64128/shadowsocks-uri-generator
21 | Public
22 | false
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 | True
37 |
38 |
39 |
40 | True
41 |
42 |
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator.CLI/Validators.cs:
--------------------------------------------------------------------------------
1 | using System.CommandLine;
2 | using System.CommandLine.Parsing;
3 |
4 | namespace ShadowsocksUriGenerator.CLI
5 | {
6 | ///
7 | /// Class for common utility validators.
8 | /// Do not put command-specific validators here.
9 | ///
10 | public static class Validators
11 | {
12 | public static Action EnforceZeroUsernamesArgumentWhenAll(Argument usernamesArgumentZeroOrMore, Option allUsersOption) => commandResult =>
13 | {
14 | bool hasUsernames = commandResult.GetValue(usernamesArgumentZeroOrMore)?.Length > 0;
15 | bool hasAllUsers = commandResult.GetValue(allUsersOption);
16 | if (!hasUsernames && !hasAllUsers)
17 | {
18 | commandResult.AddError("Please either specify target users, or use `--all-users` to target all users.");
19 | }
20 | else if (hasUsernames && hasAllUsers)
21 | {
22 | commandResult.AddError("You can't specify target users when targeting all users with `--all-users`.");
23 | }
24 | };
25 |
26 | public static Action EnforceZeroNodenamesArgumentWhenAll(Argument nodenamesArgumentZeroOrMore, Option allNodesOption) => commandResult =>
27 | {
28 | bool hasNodenames = commandResult.GetValue(nodenamesArgumentZeroOrMore)?.Length > 0;
29 | bool hasAllNodes = commandResult.GetValue(allNodesOption);
30 | EnforceZeroNodenamesWhenAll(hasNodenames, hasAllNodes, commandResult);
31 | };
32 |
33 | public static Action EnforceZeroNodenamesOptionWhenAll(Option nodenamesOption, Option allNodesOption) => commandResult =>
34 | {
35 | bool hasNodenames = commandResult.GetResult(nodenamesOption) is not null;
36 | bool hasAllNodes = commandResult.GetValue(allNodesOption);
37 | EnforceZeroNodenamesWhenAll(hasNodenames, hasAllNodes, commandResult);
38 | };
39 |
40 | private static void EnforceZeroNodenamesWhenAll(bool hasNodenames, bool hasAllNodes, CommandResult commandResult)
41 | {
42 | if (!hasNodenames && !hasAllNodes)
43 | {
44 | commandResult.AddError("Please either specify target nodes, or use `--all-nodes` to target all nodes.");
45 | }
46 | else if (hasNodenames && hasAllNodes)
47 | {
48 | commandResult.AddError("You can't specify target nodes when targeting all nodes with `--all-nodes`.");
49 | }
50 | }
51 |
52 | public static Action EnforceZeroGroupsArgumentWhenAll(Argument groupsArgumentZeroOrMore, Option allGroupsOption) => commandResult =>
53 | {
54 | bool hasGroups = commandResult.GetValue(groupsArgumentZeroOrMore)?.Length > 0;
55 | bool hasAllGroups = commandResult.GetValue(allGroupsOption);
56 | EnforceZeroGroupsWhenAll(hasGroups, hasAllGroups, commandResult);
57 | };
58 |
59 | public static Action EnforceZeroGroupsOptionWhenAll(Option groupsOption, Option allGroupsOption) => commandResult =>
60 | {
61 | bool hasGroups = commandResult.GetResult(groupsOption) is not null;
62 | bool hasAllGroups = commandResult.GetValue(allGroupsOption);
63 | EnforceZeroGroupsWhenAll(hasGroups, hasAllGroups, commandResult);
64 | };
65 |
66 | private static void EnforceZeroGroupsWhenAll(bool hasGroups, bool hasAllGroups, CommandResult commandResult)
67 | {
68 | if (!hasGroups && !hasAllGroups)
69 | {
70 | commandResult.AddError("Please either specify target groups, or use `--all-groups` to target all groups.");
71 | }
72 | else if (hasGroups && hasAllGroups)
73 | {
74 | commandResult.AddError("You can't specify target groups when targeting all users with `--all-groups`.");
75 | }
76 | }
77 |
78 | public static Action ValidateOwnerOptions(Option ownerOption, Option unsetOwnerOption) => commandResult =>
79 | {
80 | bool setOwner = commandResult.GetResult(ownerOption) is not null;
81 | bool unsetOwner = commandResult.GetValue(unsetOwnerOption);
82 |
83 | if (setOwner && unsetOwner)
84 | {
85 | commandResult.AddError("You can't set and unset owner at the same time.");
86 | }
87 | };
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator.CLI/ss-uri-gen.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/database64128/shadowsocks-uri-generator/d9bcc8eacf6293fb8ed15b72de04081584bce9af/ShadowsocksUriGenerator.CLI/ss-uri-gen.ico
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator.Chatbot.Telegram.Tests/ShadowsocksUriGenerator.Chatbot.Telegram.Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net9.0
5 |
6 | false
7 |
8 | enable
9 | enable
10 |
11 |
12 |
13 |
14 |
15 |
16 | runtime; build; native; contentfiles; analyzers; buildtransitive
17 | all
18 |
19 |
20 | runtime; build; native; contentfiles; analyzers; buildtransitive
21 | all
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator.Chatbot.Telegram/BotConfig.cs:
--------------------------------------------------------------------------------
1 | using ShadowsocksUriGenerator.Utils;
2 |
3 | namespace ShadowsocksUriGenerator.Chatbot.Telegram
4 | {
5 | public sealed class BotConfig
6 | {
7 | /// Gets the default bot config version
8 | /// used by this version of the app.
9 | ///
10 | public static int DefaultVersion => 1;
11 |
12 | ///
13 | /// Gets or sets the bot config version number.
14 | ///
15 | public int Version { get; set; } = DefaultVersion;
16 |
17 | ///
18 | /// Gets or sets the Telegram bot token.
19 | ///
20 | public string BotToken { get; set; } = "";
21 |
22 | ///
23 | /// Gets or sets the name of the service.
24 | /// The service name will be displayed in the welcome message.
25 | ///
26 | public string ServiceName { get; set; } = "";
27 |
28 | ///
29 | /// Gets or sets whether to allow any user to
30 | /// see all registered users.
31 | /// Defaults to false.
32 | ///
33 | public bool UsersCanSeeAllUsers { get; set; }
34 |
35 | ///
36 | /// Gets or sets whether to allow any user to
37 | /// see all server groups.
38 | /// Defaults to false.
39 | /// Users can only see groups they are in.
40 | /// Change to true to allow everyone to see every group.
41 | ///
42 | public bool UsersCanSeeAllGroups { get; set; }
43 |
44 | ///
45 | /// Gets or sets whether users are allowed
46 | /// to query group data usage metrics.
47 | /// Defaults to false.
48 | ///
49 | public bool UsersCanSeeGroupDataUsage { get; set; }
50 |
51 | ///
52 | /// Gets or sets whether users are allowed
53 | /// to see other group member's data limit.
54 | /// Defaults to false;
55 | ///
56 | public bool UsersCanSeeGroupDataLimit { get; set; }
57 |
58 | ///
59 | /// Gets or sets whether to allow users to associate with
60 | /// their Telegram account. Authentication is done using
61 | /// user's UUID.
62 | /// Defaults to true.
63 | ///
64 | public bool AllowChatAssociation { get; set; } = true;
65 |
66 | ///
67 | /// Gets or sets the dictionary to store chat association data.
68 | /// Key is Telegram user ID.
69 | /// Value is user UUID.
70 | ///
71 | public Dictionary ChatAssociations { get; set; } = [];
72 |
73 | ///
74 | /// Loads bot config from TelegramBotConfig.json.
75 | ///
76 | /// A token that may be used to cancel the read operation.
77 | ///
78 | /// A ValueTuple containing a object and an optional error message.
79 | ///
80 | public static async Task<(BotConfig botConfig, string? errMsg)> LoadBotConfigAsync(CancellationToken cancellationToken = default)
81 | {
82 | var (botConfig, errMsg) = await FileHelper.LoadJsonAsync("TelegramBotConfig.json", BotConfigJsonSerializerContext.Default.BotConfig, cancellationToken);
83 | if (errMsg is null && botConfig.Version != DefaultVersion)
84 | {
85 | botConfig.UpdateBotConfig();
86 | errMsg = await SaveBotConfigAsync(botConfig, cancellationToken);
87 | }
88 | return (botConfig, errMsg);
89 | }
90 |
91 | ///
92 | /// Saves bot config to TelegramBotConfig.json.
93 | ///
94 | /// The object to save.
95 | /// A token that may be used to cancel the write operation.
96 | ///
97 | /// An optional error message.
98 | /// Null if no errors occurred.
99 | ///
100 | public static Task SaveBotConfigAsync(BotConfig botConfig, CancellationToken cancellationToken = default)
101 | => FileHelper.SaveJsonAsync("TelegramBotConfig.json", botConfig, BotConfigJsonSerializerContext.Default.BotConfig, false, false, cancellationToken);
102 |
103 | ///
104 | /// Updates the current object to the latest version.
105 | ///
106 | public void UpdateBotConfig()
107 | {
108 | switch (Version)
109 | {
110 | case 0: // nothing to do
111 | Version++;
112 | goto default; // go to the next update path
113 | default:
114 | break;
115 | }
116 | }
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator.Chatbot.Telegram/BotConfigJsonSerializerContext.cs:
--------------------------------------------------------------------------------
1 | using System.Text.Json;
2 | using System.Text.Json.Serialization;
3 |
4 | namespace ShadowsocksUriGenerator.Chatbot.Telegram;
5 |
6 | [JsonSerializable(typeof(BotConfig))]
7 | [JsonSourceGenerationOptions(
8 | AllowTrailingCommas = true,
9 | IgnoreReadOnlyProperties = true,
10 | ReadCommentHandling = JsonCommentHandling.Skip,
11 | WriteIndented = true)]
12 | public partial class BotConfigJsonSerializerContext : JsonSerializerContext
13 | {
14 | }
15 |
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator.Chatbot.Telegram/CLI/BotRunner.cs:
--------------------------------------------------------------------------------
1 | using Telegram.Bot;
2 | using Telegram.Bot.Polling;
3 | using Telegram.Bot.Types;
4 |
5 | namespace ShadowsocksUriGenerator.Chatbot.Telegram.CLI
6 | {
7 | public static class BotRunner
8 | {
9 | public static async Task RunBot(string? botToken, CancellationToken cancellationToken = default)
10 | {
11 | var (botConfig, loadBotConfigErrMsg) = await BotConfig.LoadBotConfigAsync(cancellationToken);
12 | if (loadBotConfigErrMsg is not null)
13 | {
14 | Console.WriteLine(loadBotConfigErrMsg);
15 | return 1;
16 | }
17 |
18 | // Priority: commandline option > environment variable > config file
19 | if (string.IsNullOrEmpty(botToken))
20 | botToken = Environment.GetEnvironmentVariable("TELEGRAM_BOT_TOKEN");
21 | if (string.IsNullOrEmpty(botToken))
22 | botToken = botConfig.BotToken;
23 | if (string.IsNullOrEmpty(botToken))
24 | {
25 | Console.WriteLine("Please provide a bot token with command line option `--bot-token`, environment variable `TELEGRAM_BOT_TOKEN`, or in the config file.");
26 | return -1;
27 | }
28 |
29 | try
30 | {
31 | var bot = new TelegramBotClient(botToken);
32 | Console.WriteLine("Created Telegram bot instance with API token.");
33 |
34 | var me = await bot.GetMe(cancellationToken);
35 | if (string.IsNullOrEmpty(me.Username))
36 | throw new Exception("Error: bot username is null or empty.");
37 |
38 | await bot.SetMyCommands(UpdateHandler.BotCommandsPublic, null, null, cancellationToken);
39 | Console.WriteLine($"Registered {UpdateHandler.BotCommandsPublic.Length} bot commands for all chats.");
40 |
41 | var privateChatCommands = UpdateHandler.BotCommandsPrivate.Concat(UpdateHandler.BotCommandsPublic);
42 | await bot.SetMyCommands(privateChatCommands, BotCommandScope.AllPrivateChats(), null, cancellationToken);
43 | Console.WriteLine($"Registered {privateChatCommands.Count()} bot commands for private chats.");
44 |
45 | Console.WriteLine($"Started Telegram bot: @{me.Username} ({me.Id}).");
46 |
47 | var updateHandler = new UpdateHandler(me.Username, botConfig);
48 | var updateReceiver = new QueuedUpdateReceiver(bot, null, UpdateHandler.HandleErrorAsync);
49 | await updateHandler.HandleUpdateStreamAsync(bot, updateReceiver, cancellationToken);
50 | }
51 | catch (ArgumentException ex)
52 | {
53 | Console.WriteLine($"Invalid access token: {ex.Message}");
54 | }
55 | catch (HttpRequestException ex)
56 | {
57 | Console.WriteLine($"A network error occurred: {ex.Message}");
58 | }
59 | catch (Exception ex)
60 | {
61 | Console.WriteLine(ex);
62 | }
63 |
64 | return 0;
65 | }
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator.Chatbot.Telegram/CLI/ConfigCommand.cs:
--------------------------------------------------------------------------------
1 | using ShadowsocksUriGenerator.CLI.Utils;
2 |
3 | namespace ShadowsocksUriGenerator.Chatbot.Telegram.CLI
4 | {
5 | public static class ConfigCommand
6 | {
7 | public static async Task Get(CancellationToken cancellationToken = default)
8 | {
9 | var (botConfig, loadBotConfigErrMsg) = await BotConfig.LoadBotConfigAsync(cancellationToken);
10 | if (loadBotConfigErrMsg is not null)
11 | {
12 | Console.WriteLine(loadBotConfigErrMsg);
13 | return 1;
14 | }
15 |
16 | ConsoleHelper.PrintTableBorder(28, 50);
17 | Console.WriteLine($"|{"Key",-28}|{"Value",50}|");
18 | ConsoleHelper.PrintTableBorder(28, 50);
19 |
20 | Console.WriteLine($"|{"Version",-28}|{botConfig.Version,50}|");
21 | Console.WriteLine($"|{"BotToken",-28}|{botConfig.BotToken,50}|");
22 | Console.WriteLine($"|{"ServiceName",-28}|{botConfig.ServiceName,50}|");
23 | Console.WriteLine($"|{"UsersCanSeeAllUsers",-28}|{botConfig.UsersCanSeeAllUsers,50}|");
24 | Console.WriteLine($"|{"UsersCanSeeAllGroups",-28}|{botConfig.UsersCanSeeAllGroups,50}|");
25 | Console.WriteLine($"|{"UsersCanSeeGroupDataUsage",-28}|{botConfig.UsersCanSeeGroupDataUsage,50}|");
26 | Console.WriteLine($"|{"UsersCanSeeGroupDataLimit",-28}|{botConfig.UsersCanSeeGroupDataLimit,50}|");
27 | Console.WriteLine($"|{"AllowChatAssociation",-28}|{botConfig.AllowChatAssociation,50}|");
28 |
29 | ConsoleHelper.PrintTableBorder(28, 50);
30 |
31 | return 0;
32 | }
33 |
34 | public static async Task Set(string? botToken, string? serviceName, bool? usersCanSeeAllUsers, bool? usersCanSeeAllGroups, bool? usersCanSeeGroupDataUsage, bool? usersCanSeeGroupDataLimit, bool? allowChatAssociation, CancellationToken cancellationToken = default)
35 | {
36 | var (botConfig, loadBotConfigErrMsg) = await BotConfig.LoadBotConfigAsync(cancellationToken);
37 | if (loadBotConfigErrMsg is not null)
38 | {
39 | Console.WriteLine(loadBotConfigErrMsg);
40 | return 1;
41 | }
42 |
43 | if (!string.IsNullOrEmpty(botToken))
44 | botConfig.BotToken = botToken;
45 | if (!string.IsNullOrEmpty(serviceName))
46 | botConfig.ServiceName = serviceName;
47 | if (usersCanSeeAllUsers is bool canSeeUsers)
48 | botConfig.UsersCanSeeAllUsers = canSeeUsers;
49 | if (usersCanSeeAllGroups is bool canSeeGroups)
50 | botConfig.UsersCanSeeAllGroups = canSeeGroups;
51 | if (usersCanSeeGroupDataUsage is bool canSeeGroupDataUsage)
52 | botConfig.UsersCanSeeGroupDataUsage = canSeeGroupDataUsage;
53 | if (usersCanSeeGroupDataLimit is bool canSeeGroupDataLimit)
54 | botConfig.UsersCanSeeGroupDataLimit = canSeeGroupDataLimit;
55 | if (allowChatAssociation is bool allowLinking)
56 | botConfig.AllowChatAssociation = allowLinking;
57 |
58 | var saveBotConfigErrMsg = await BotConfig.SaveBotConfigAsync(botConfig, cancellationToken);
59 | if (saveBotConfigErrMsg is not null)
60 | {
61 | Console.WriteLine(saveBotConfigErrMsg);
62 | return 1;
63 | }
64 |
65 | return 0;
66 | }
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator.Chatbot.Telegram/Commands/AuthCommands.cs:
--------------------------------------------------------------------------------
1 | using ShadowsocksUriGenerator.Chatbot.Telegram.Utils;
2 | using ShadowsocksUriGenerator.Data;
3 | using Telegram.Bot;
4 | using Telegram.Bot.Types;
5 | using Telegram.Bot.Types.Enums;
6 |
7 | namespace ShadowsocksUriGenerator.Chatbot.Telegram.Commands
8 | {
9 | public static class AuthCommands
10 | {
11 | public static Task StartAsync(ITelegramBotClient botClient, Message message, BotConfig config, CancellationToken cancellationToken = default)
12 | {
13 | Console.WriteLine($"{message.From} executed {message.Text} in {message.Chat.Type.ToString().ToLower()} chat {(string.IsNullOrEmpty(message.Chat.Title) ? string.Empty : $"{message.Chat.Title} ")}({message.Chat.Id}).");
14 |
15 | var serviceName = string.IsNullOrEmpty(config.ServiceName) ? "with us" : config.ServiceName;
16 |
17 | var replyMarkdownV2 = $@"🧑✈️ Good evening\! Thank you for flying {serviceName}\.
18 |
19 | ✈️ To get your boarding pass, please use `/link ` to link your Telegram account to your user\.";
20 |
21 | return botClient.SendMessage(
22 | message.Chat.Id,
23 | replyMarkdownV2,
24 | parseMode: ParseMode.MarkdownV2,
25 | replyParameters: message,
26 | cancellationToken: cancellationToken);
27 | }
28 |
29 | public static async Task LinkAsync(ITelegramBotClient botClient, Message message, string? argument, CancellationToken cancellationToken = default)
30 | {
31 | Console.Write($"{message.From} executed {message.Text} in {message.Chat.Type.ToString().ToLower()} chat {(string.IsNullOrEmpty(message.Chat.Title) ? string.Empty : $"{message.Chat.Title} ")}({message.Chat.Id}).");
32 |
33 | string reply;
34 |
35 | var (botConfig, loadBotConfigErrMsg) = await BotConfig.LoadBotConfigAsync(cancellationToken);
36 | if (loadBotConfigErrMsg is not null)
37 | {
38 | Console.WriteLine();
39 | Console.WriteLine(loadBotConfigErrMsg);
40 | return;
41 | }
42 |
43 | if (!botConfig.AllowChatAssociation)
44 | {
45 | reply = "The admin has disabled Telegram association.";
46 | Console.WriteLine(" Response: command disabled.");
47 | }
48 | else if (message.Chat.Type != ChatType.Private || message.From is null)
49 | {
50 | reply = "Associations must be made in a private chat.";
51 | Console.WriteLine(" Response: not in private chat.");
52 | }
53 | else if (string.IsNullOrEmpty(argument))
54 | {
55 | reply = "Please provide your user UUID as the command argument.";
56 | Console.WriteLine(" Response: missing argument.");
57 | }
58 | else
59 | {
60 | var (users, loadUsersErrMsg) = await Users.LoadUsersAsync(cancellationToken);
61 | if (loadUsersErrMsg is not null)
62 | {
63 | Console.WriteLine();
64 | Console.WriteLine(loadUsersErrMsg);
65 | return;
66 | }
67 |
68 | if (!DataHelper.TryLocateUserFromUuid(argument, users, out var matchedUser))
69 | {
70 | reply = "User not found.";
71 | Console.WriteLine(" Response: user not found.");
72 | }
73 | else if (botConfig.ChatAssociations.TryGetValue(message.From.Id, out var userUuid))
74 | {
75 | if (userUuid == matchedUser.Value.Value.Uuid)
76 | {
77 | reply = $"You are already linked to {matchedUser.Value.Key}.";
78 | Console.WriteLine(" Response: already linked.");
79 | }
80 | else
81 | {
82 | reply = $"You are already linked to another user with UUID {userUuid}.";
83 | Console.WriteLine(" Response: already linked to another user.");
84 | }
85 | }
86 | else
87 | {
88 | botConfig.ChatAssociations.Add(message.From.Id, matchedUser.Value.Value.Uuid);
89 |
90 | reply = $"Successfully linked your Telegram account to {matchedUser.Value.Key}.";
91 | Console.WriteLine(" Response: success.");
92 |
93 | var saveBotConfigErrMsg = await BotConfig.SaveBotConfigAsync(botConfig, cancellationToken);
94 | if (saveBotConfigErrMsg is not null)
95 | {
96 | Console.WriteLine(saveBotConfigErrMsg);
97 | return;
98 | }
99 | }
100 | }
101 |
102 | _ = await botClient.SendMessage(
103 | message.Chat.Id,
104 | reply,
105 | replyParameters: message,
106 | cancellationToken: cancellationToken);
107 | }
108 |
109 | public static async Task UnlinkAsync(ITelegramBotClient botClient, Message message, CancellationToken cancellationToken = default)
110 | {
111 | Console.Write($"{message.From} executed {message.Text} in {message.Chat.Type.ToString().ToLower()} chat {(string.IsNullOrEmpty(message.Chat.Title) ? string.Empty : $"{message.Chat.Title} ")}({message.Chat.Id}).");
112 |
113 | string reply;
114 |
115 | var (botConfig, loadBotConfigErrMsg) = await BotConfig.LoadBotConfigAsync(cancellationToken);
116 | if (loadBotConfigErrMsg is not null)
117 | {
118 | Console.WriteLine();
119 | Console.WriteLine(loadBotConfigErrMsg);
120 | return;
121 | }
122 |
123 | if (!botConfig.AllowChatAssociation)
124 | {
125 | reply = "The admin has disabled Telegram association.";
126 | Console.WriteLine(" Response: command disabled.");
127 | }
128 | else if (message.Chat.Type != ChatType.Private || message.From is null)
129 | {
130 | reply = "Associations must be made in a private chat.";
131 | Console.WriteLine(" Response: not in private chat.");
132 | }
133 | else if (botConfig.ChatAssociations.Remove(message.From.Id, out var userUuid))
134 | {
135 | reply = $"Successfully unlinked your Telegram account from {userUuid}.";
136 | Console.WriteLine(" Response: success.");
137 |
138 | var saveBotConfigErrMsg = await BotConfig.SaveBotConfigAsync(botConfig, cancellationToken);
139 | if (saveBotConfigErrMsg is not null)
140 | {
141 | Console.WriteLine(saveBotConfigErrMsg);
142 | return;
143 | }
144 | }
145 | else
146 | {
147 | reply = "You are not linked to any user.";
148 | Console.WriteLine(" Response: not linked");
149 | }
150 |
151 | _ = await botClient.SendMessage(
152 | message.Chat.Id,
153 | reply,
154 | replyParameters: message,
155 | cancellationToken: cancellationToken);
156 | }
157 | }
158 | }
159 |
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator.Chatbot.Telegram/Program.cs:
--------------------------------------------------------------------------------
1 | using ShadowsocksUriGenerator.Chatbot.Telegram.CLI;
2 | using System.CommandLine;
3 | using System.Text;
4 |
5 | namespace ShadowsocksUriGenerator.Chatbot.Telegram;
6 |
7 | internal class Program
8 | {
9 | private static Task Main(string[] args)
10 | {
11 | var botTokenOption = new Option("--bot-token")
12 | {
13 | Description = "Telegram bot token.",
14 | };
15 | var serviceNameOption = new Option("--service-name")
16 | {
17 | Description = "Service name. Will be displayed in the welcome message.",
18 | };
19 | var usersCanSeeAllUsersOption = new Option("--users-can-see-all-users")
20 | {
21 | Description = "Whether any registered user is allowed to see all registered users.",
22 | };
23 | var usersCanSeeAllGroupsOption = new Option("--users-can-see-all-groups")
24 | {
25 | Description = "Whether any registered user is allowed to see all groups.",
26 | };
27 | var usersCanSeeGroupDataUsageOption = new Option("--users-can-see-group-data-usage")
28 | {
29 | Description = "Whether users are allowed to query group data usage metrics.",
30 | };
31 | var usersCanSeeGroupDataLimitOption = new Option("--users-can-see-group-data-limit")
32 | {
33 | Description = "Whether users are allowed to see other group member's data limit.",
34 | };
35 | var allowChatAssociationOption = new Option("--allow-chat-association")
36 | {
37 | Description = "Whether Telegram association through /link in chat is allowed.",
38 | };
39 |
40 | var configGetCommand = new Command("get", "Get and print bot config.");
41 |
42 | var configSetCommand = new Command("set", "Change bot config.")
43 | {
44 | botTokenOption,
45 | serviceNameOption,
46 | usersCanSeeAllUsersOption,
47 | usersCanSeeAllGroupsOption,
48 | usersCanSeeGroupDataUsageOption,
49 | usersCanSeeGroupDataLimitOption,
50 | allowChatAssociationOption,
51 | };
52 |
53 | configGetCommand.SetAction((_, cancellationToken) => ConfigCommand.Get(cancellationToken));
54 | configSetCommand.SetAction((parseResult, cancellationToken) =>
55 | {
56 | var botToken = parseResult.GetValue(botTokenOption);
57 | var serviceName = parseResult.GetValue(serviceNameOption);
58 | var usersCanSeeAllUsers = parseResult.GetValue(usersCanSeeAllUsersOption);
59 | var usersCanSeeAllGroups = parseResult.GetValue(usersCanSeeAllGroupsOption);
60 | var usersCanSeeGroupDataUsage = parseResult.GetValue(usersCanSeeGroupDataUsageOption);
61 | var usersCanSeeGroupDataLimit = parseResult.GetValue(usersCanSeeGroupDataLimitOption);
62 | var allowChatAssociation = parseResult.GetValue(allowChatAssociationOption);
63 | return ConfigCommand.Set(botToken, serviceName, usersCanSeeAllUsers, usersCanSeeAllGroups, usersCanSeeGroupDataUsage, usersCanSeeGroupDataLimit, allowChatAssociation, cancellationToken);
64 | });
65 |
66 | var configCommand = new Command("config", "Print or change bot config.")
67 | {
68 | configGetCommand,
69 | configSetCommand,
70 | };
71 |
72 | var rootCommand = new RootCommand("A Telegram bot for user interactions with Shadowsocks URI Generator.")
73 | {
74 | configCommand,
75 | };
76 |
77 | rootCommand.Options.Add(botTokenOption);
78 | rootCommand.SetAction((parseResult, cancellationToken) =>
79 | {
80 | var botToken = parseResult.GetValue(botTokenOption);
81 | return BotRunner.RunBot(botToken, cancellationToken);
82 | });
83 |
84 | Console.OutputEncoding = Encoding.UTF8;
85 | return rootCommand.Parse(args).InvokeAsync();
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator.Chatbot.Telegram/ShadowsocksUriGenerator.Chatbot.Telegram.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | net9.0
6 | enable
7 | enable
8 | true
9 | ss-uri-gen-chatbot-telegram
10 | ss-uri-gen-chatbot-telegram.ico
11 | ShadowsocksUriGenerator.Chatbot.Telegram
12 | 5.2.0
13 | database64128
14 | Shadowsocks URI Generator - Telegram Chatbot
15 | A Telegram bot for user interactions with Shadowsocks URI Generator.
16 | © 2023 database64128
17 | LICENSE
18 | https://github.com/database64128/shadowsocks-uri-generator
19 | ss-uri-gen-chatbot-telegram.png
20 | https://github.com/database64128/shadowsocks-uri-generator
21 | Public
22 | false
23 |
24 |
25 |
26 |
27 | True
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator.Chatbot.Telegram/UpdateHandler.cs:
--------------------------------------------------------------------------------
1 | using ShadowsocksUriGenerator.Chatbot.Telegram.Commands;
2 | using ShadowsocksUriGenerator.Chatbot.Telegram.Utils;
3 | using Telegram.Bot;
4 | using Telegram.Bot.Exceptions;
5 | using Telegram.Bot.Types;
6 | using Telegram.Bot.Types.Enums;
7 |
8 | namespace ShadowsocksUriGenerator.Chatbot.Telegram
9 | {
10 | public sealed class UpdateHandler(string botUsername, BotConfig botConfig)
11 | {
12 | ///
13 | /// Gets public bot commands that are available to all types of chats.
14 | ///
15 | public static BotCommand[] BotCommandsPublic =>
16 | [
17 | new() { Command = "start", Description = "Cleared for takeoff!", },
18 | new() { Command = "list_users", Description = "List all users", },
19 | new() { Command = "list_nodes", Description = "List all nodes or nodes from the specified group", },
20 | new() { Command = "list_groups", Description = "List all groups", },
21 | new() { Command = "list_group_members", Description = "List members of the specified group", },
22 | new() { Command = "list_owned_nodes", Description = "List nodes owned by you or the specified user", },
23 | new() { Command = "list_owned_groups", Description = "List groups owned by you or the specified user", },
24 | new() { Command = "get_user_data_usage", Description = "Get data usage statistics of the associated user or the specified user", },
25 | new() { Command = "get_user_data_limit", Description = "Get data limit settings of the associated user or the specified user", },
26 | new() { Command = "get_group_data_usage", Description = "Get data usage statistics of groups you own or the specified group", },
27 | new() { Command = "get_group_data_limit", Description = "Get data limit settings of the specified group", },
28 | new() { Command = "report", Description = "Generate server usage report", },
29 | new() { Command = "report_csv", Description = "Generate server usage report in CSV format", },
30 | ];
31 |
32 | ///
33 | /// Gets private bot commands that are only available to private chats.
34 | ///
35 | public static BotCommand[] BotCommandsPrivate =>
36 | [
37 | new() { Command = "link", Description = "Link your Telegram account to a user", },
38 | new() { Command = "unlink", Description = "Unlink your Telegram account from the user", },
39 | new() { Command = "get_ss_links", Description = "Get your ss:// links to all servers or servers from the specified group", },
40 | new() { Command = "get_online_config_links", Description = "Get online config API URLs and tokens", },
41 | new() { Command = "get_credentials", Description = "Get your credentials to all servers or servers from the specified group", },
42 | ];
43 |
44 | public async Task HandleUpdateStreamAsync(ITelegramBotClient botClient, IAsyncEnumerable updates, CancellationToken cancellationToken = default)
45 | {
46 | await foreach (var update in updates.WithCancellation(cancellationToken))
47 | {
48 | try
49 | {
50 | if (update.Type == UpdateType.Message && update.Message is not null)
51 | {
52 | await HandleCommandAsync(botClient, update.Message, cancellationToken);
53 | }
54 | }
55 | catch (Exception ex)
56 | {
57 | HandleError(ex);
58 | }
59 | }
60 | }
61 |
62 | public Task HandleCommandAsync(ITelegramBotClient botClient, Message message, CancellationToken cancellationToken = default)
63 | {
64 | var (command, argument) = ChatHelper.ParseMessageIntoCommandAndArgument(message.Text, botUsername);
65 |
66 | // Handle command
67 | return command switch
68 | {
69 | "start" => AuthCommands.StartAsync(botClient, message, botConfig, cancellationToken),
70 | "link" => AuthCommands.LinkAsync(botClient, message, argument, cancellationToken),
71 | "unlink" => AuthCommands.UnlinkAsync(botClient, message, cancellationToken),
72 | "list_users" => ListCommands.ListUsersAsync(botClient, message, cancellationToken),
73 | "list_nodes" => ListCommands.ListNodesAsync(botClient, message, argument, cancellationToken),
74 | "list_groups" => ListCommands.ListGroupsAsync(botClient, message, cancellationToken),
75 | "list_group_members" => ListCommands.ListGroupMembersAsync(botClient, message, argument, cancellationToken),
76 | "list_owned_nodes" => ListCommands.ListOwnedNodesAsync(botClient, message, argument, cancellationToken),
77 | "list_owned_groups" => ListCommands.ListOwnedGroupsAsync(botClient, message, argument, cancellationToken),
78 | "get_user_data_usage" => DataCommands.GetUserDataUsageAsync(botClient, message, argument, cancellationToken),
79 | "get_user_data_limit" => DataCommands.GetUserDataLimitAsync(botClient, message, argument, cancellationToken),
80 | "get_group_data_usage" => DataCommands.GetGroupDataUsageAsync(botClient, message, argument, cancellationToken),
81 | "get_group_data_limit" => DataCommands.GetGroupDataLimitAsync(botClient, message, argument, cancellationToken),
82 | "get_ss_links" => CredCommands.GetSsLinksAsync(botClient, message, argument, cancellationToken),
83 | "get_online_config_links" => CredCommands.GetOnlineConfigLinksAsync(botClient, message, cancellationToken),
84 | "get_credentials" => CredCommands.GetCredentialsAsync(botClient, message, argument, cancellationToken),
85 | "report" => ReportCommand.GenerateReportAsync(botClient, message, argument, cancellationToken),
86 | "report_csv" => ReportCommand.GenerateReportAsync(botClient, message, "csv", cancellationToken),
87 | _ => Task.CompletedTask, // unrecognized command, ignoring
88 | };
89 | }
90 |
91 | public static void HandleError(Exception ex)
92 | {
93 | var errorMessage = ex switch
94 | {
95 | ApiRequestException apiRequestException => $"Telegram API Error: [{apiRequestException.ErrorCode}] {apiRequestException.Message}",
96 | _ => ex.ToString(),
97 | };
98 |
99 | Console.WriteLine(errorMessage);
100 | }
101 |
102 | public static Task HandleErrorAsync(Exception ex, CancellationToken _ = default)
103 | {
104 | HandleError(ex);
105 | return Task.CompletedTask;
106 | }
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator.Chatbot.Telegram/Utils/DataHelper.cs:
--------------------------------------------------------------------------------
1 | using ShadowsocksUriGenerator.Data;
2 | using System.Diagnostics.CodeAnalysis;
3 |
4 | namespace ShadowsocksUriGenerator.Chatbot.Telegram.Utils
5 | {
6 | public static class DataHelper
7 | {
8 | public static bool TryLocateUserFromUuid(string userUuid, Users users, [NotNullWhen(true)] out KeyValuePair? userEntry)
9 | {
10 | var userSearchResult = users.UserDict.Where(x => string.Equals(x.Value.Uuid, userUuid, StringComparison.OrdinalIgnoreCase));
11 | if (userSearchResult.Any())
12 | {
13 | userEntry = userSearchResult.First();
14 | return true;
15 | }
16 | else
17 | {
18 | userEntry = null;
19 | return false;
20 | }
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator.Chatbot.Telegram/ss-uri-gen-chatbot-telegram.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/database64128/shadowsocks-uri-generator/d9bcc8eacf6293fb8ed15b72de04081584bce9af/ShadowsocksUriGenerator.Chatbot.Telegram/ss-uri-gen-chatbot-telegram.ico
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator.Chatbot.Telegram/ss-uri-gen-chatbot-telegram.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/database64128/shadowsocks-uri-generator/d9bcc8eacf6293fb8ed15b72de04081584bce9af/ShadowsocksUriGenerator.Chatbot.Telegram/ss-uri-gen-chatbot-telegram.png
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator.Rescue.CLI/Program.cs:
--------------------------------------------------------------------------------
1 | using System.CommandLine;
2 | using System.Text;
3 |
4 | namespace ShadowsocksUriGenerator.Rescue.CLI;
5 |
6 | internal class Program
7 | {
8 | private static Task Main(string[] args)
9 | {
10 | var onlineConfigDirOption = new Option("--online-config-dir")
11 | {
12 | Description = "Directory of generated online config.",
13 | Required = true,
14 | };
15 | var outputDirOption = new Option("--output-dir")
16 | {
17 | Description = "Output directory.",
18 | Required = true,
19 | };
20 |
21 | var rootCommand = new RootCommand("A rescue tool CLI for restoring ss-uri-gen config from generated online config directory.")
22 | {
23 | onlineConfigDirOption,
24 | outputDirOption,
25 | };
26 |
27 | rootCommand.SetAction((parseResult, cancellationToken) =>
28 | {
29 | var onlineConfigDir = parseResult.GetValue(onlineConfigDirOption)!;
30 | var outputDir = parseResult.GetValue(onlineConfigDirOption)!;
31 | return HandleRootCommand(onlineConfigDir, outputDir, cancellationToken);
32 | });
33 |
34 | Console.OutputEncoding = Encoding.UTF8;
35 | return rootCommand.Parse(args).InvokeAsync();
36 | }
37 |
38 | private static async Task HandleRootCommand(string onlineConfigDir, string outputDir, CancellationToken cancellationToken = default)
39 | {
40 | var (users, nodes, errMsgGetFromOC) = await Rescuers.FromOnlineConfig(onlineConfigDir, cancellationToken);
41 | if (errMsgGetFromOC is not null || users is null || nodes is null)
42 | {
43 | Console.WriteLine(errMsgGetFromOC);
44 | return -1;
45 | }
46 |
47 | var errMsgSaveJson = await Restorers.ToJsonFiles(outputDir, users, nodes, cancellationToken);
48 | if (errMsgSaveJson is not null)
49 | {
50 | Console.WriteLine(errMsgSaveJson);
51 | return -2;
52 | }
53 |
54 | return 0;
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator.Rescue.CLI/ShadowsocksUriGenerator.Rescue.CLI.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | net9.0
6 | enable
7 | enable
8 | true
9 | ss-uri-gen-rescue
10 | ss-uri-gen-rescue.ico
11 | ShadowsocksUriGenerator.Rescue.CLI
12 | 5.2.0
13 | database64128
14 | Shadowsocks URI Generator Rescue CLI
15 | A rescue tool CLI for restoring ss-uri-gen config from generated online config directory.
16 | © 2023 database64128
17 | LICENSE
18 | https://github.com/database64128/shadowsocks-uri-generator
19 | ss-uri-gen-rescue.png
20 | https://github.com/database64128/shadowsocks-uri-generator
21 | Public
22 | false
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | True
36 |
37 |
38 |
39 | True
40 |
41 |
42 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator.Rescue.CLI/ss-uri-gen-rescue.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/database64128/shadowsocks-uri-generator/d9bcc8eacf6293fb8ed15b72de04081584bce9af/ShadowsocksUriGenerator.Rescue.CLI/ss-uri-gen-rescue.ico
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator.Rescue/Restorers.cs:
--------------------------------------------------------------------------------
1 | using ShadowsocksUriGenerator.Data;
2 | using ShadowsocksUriGenerator.Utils;
3 |
4 | namespace ShadowsocksUriGenerator.Rescue
5 | {
6 | ///
7 | /// Class for static methods that
8 | /// restore order from information collected by rescuers.
9 | ///
10 | public static class Restorers
11 | {
12 | ///
13 | /// Restores rescued and
14 | /// to JSON config files.
15 | ///
16 | /// Path to JSON config directory.
17 | /// The rescued object.
18 | /// The rescued object.
19 | /// A token that may be used to cancel the write operation.
20 | /// An error message. Null if no errors occurred.
21 | public static async Task ToJsonFiles(string configDir, Users users, Nodes nodes, CancellationToken cancellationToken = default)
22 | {
23 | // Make sure configDir is either empty or a path that ends with a slash.
24 | if (!string.IsNullOrEmpty(configDir) && !(configDir.EndsWith('/') || configDir.EndsWith('\\')))
25 | configDir = $"{configDir}/";
26 |
27 | var usersErrMsg = await FileHelper.SaveJsonAsync($"{configDir}Users.json",
28 | users,
29 | DataJsonSerializerContext.Default.Users,
30 | false,
31 | false,
32 | cancellationToken);
33 | var nodesErrMsg = await FileHelper.SaveJsonAsync($"{configDir}Nodes.json",
34 | nodes,
35 | DataJsonSerializerContext.Default.Nodes,
36 | false,
37 | false,
38 | cancellationToken);
39 |
40 | if (usersErrMsg is not null && nodesErrMsg is not null)
41 | return $"{usersErrMsg}{Environment.NewLine}{nodesErrMsg}";
42 | else if (usersErrMsg is not null)
43 | return usersErrMsg;
44 | else if (nodesErrMsg is not null)
45 | return nodesErrMsg;
46 | else
47 | return null;
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator.Rescue/ShadowsocksUriGenerator.Rescue.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net9.0
5 | enable
6 | enable
7 | true
8 | 5.2.0
9 | database64128
10 | Shadowsocks URI Generator Rescue Library
11 | A rescue tool library for restoring ss-uri-gen config from generated online config directory.
12 | © 2023 database64128
13 | LICENSE
14 | https://github.com/database64128/shadowsocks-uri-generator
15 | ss-uri-gen-rescue.png
16 | https://github.com/database64128/shadowsocks-uri-generator
17 | Public
18 | false
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | True
28 |
29 |
30 |
31 | True
32 |
33 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator.Rescue/ss-uri-gen-rescue.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/database64128/shadowsocks-uri-generator/d9bcc8eacf6293fb8ed15b72de04081584bce9af/ShadowsocksUriGenerator.Rescue/ss-uri-gen-rescue.png
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator.Server/Controllers/OOCv1Controller.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Mvc;
2 | using ShadowsocksUriGenerator.OnlineConfig;
3 | using ShadowsocksUriGenerator.Services;
4 |
5 | namespace ShadowsocksUriGenerator.Server.Controllers;
6 |
7 | [ApiController]
8 | [ApiConventionType(typeof(DefaultApiConventions))]
9 | [Route("ooc/v1")]
10 | public class OOCv1Controller(ILogger logger, IDataService dataService) : OnlineConfigControllerBase(logger, dataService)
11 | {
12 |
13 | ///
14 | /// Gets online config by user ID in Open Online Config 1 format.
15 | ///
16 | ///
17 | /// Returns the online config document.
18 | ///
19 | /// GET /[secret]/ooc/v1/[user_id]
20 | /// {
21 | /// "username": "database64128",
22 | /// "bytesUsed": 52940262597,
23 | /// "bytesRemaining": 52940262597,
24 | /// "protocols": [ "shadowsocks" ],
25 | /// "shadowsocks": [
26 | /// {
27 | /// "id": "27b8a625-4f4b-4428-9f0f-8a2317db7c79",
28 | /// "name": "ServerName",
29 | /// "owner": "database64128",
30 | /// "group": "examples",
31 | /// "tags": [ "direct" ],
32 | /// "address": "example.com",
33 | /// "port": 8388,
34 | /// "method": "2022-blake3-aes-256-gcm",
35 | /// "password": "z7by/oMFjG7sunqq2q69hlGynqkrgk9bCKoWp29zhgw=",
36 | /// "pluginName": "plugin-name",
37 | /// "pluginVersion": "1.0",
38 | /// "pluginOptions": "whatever",
39 | /// "pluginArguments": "-vvvvvv"
40 | /// }
41 | /// ]
42 | /// }
43 | ///
44 | ///
45 | /// User ID.
46 | /// Select nodes that contain all tags in this array.
47 | /// Select nodes from groups in this array.
48 | /// Select nodes from groups that belong to users in this array.
49 | /// Select nodes that belong to users in this array.
50 | /// Whether to sort nodes by name. Defaults to false, or no sorting.
51 | /// The online config document in Open Online Config 1 format.
52 | /// Returns the online config document.
53 | /// One or more queries contain invalid values.
54 | /// The provided user ID doesn't exist.
55 | [HttpGet("{id}")]
56 | [ProducesResponseType(StatusCodes.Status200OK)]
57 | [ProducesResponseType(StatusCodes.Status400BadRequest)]
58 | [ProducesResponseType(StatusCodes.Status404NotFound)]
59 | public ActionResult GetByUserId(string id, [FromQuery] string[] tag, [FromQuery] string[] group, [FromQuery] string[] groupOwner, [FromQuery] string[] nodeOwner, [FromQuery] bool sortByName)
60 | {
61 | if (!TryGetUserEntry(id, group, groupOwner, nodeOwner, out var username, out var user, out var targetGroupOwnerIds, out var targetNodeOwnerIds, out var objectResult))
62 | return objectResult;
63 |
64 | var servers = user.GetShadowsocksServers(DataService.UsersData, DataService.NodesData, group, tag, targetGroupOwnerIds, targetNodeOwnerIds);
65 |
66 | if (sortByName)
67 | servers = servers.OrderBy(x => x.Name);
68 |
69 | return new OOCv1ShadowsocksConfig()
70 | {
71 | Username = username,
72 | BytesUsed = user.BytesUsed > 0UL ? user.BytesUsed : null,
73 | BytesRemaining = user.BytesRemaining > 0UL ? user.BytesRemaining : null,
74 | Shadowsocks = servers.Select(x => new OOCv1ShadowsocksServer(x)),
75 | };
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator.Server/Controllers/OnlineConfigControllerBase.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Mvc;
2 | using ShadowsocksUriGenerator.Data;
3 | using ShadowsocksUriGenerator.Server.Utils;
4 | using ShadowsocksUriGenerator.Services;
5 | using System.Diagnostics.CodeAnalysis;
6 | using System.Net;
7 |
8 | namespace ShadowsocksUriGenerator.Server.Controllers;
9 |
10 | public abstract partial class OnlineConfigControllerBase(ILogger logger, IDataService dataService) : ControllerBase
11 | {
12 | protected IDataService DataService => dataService;
13 |
14 | protected bool TryGetUserEntry(
15 | string id,
16 | string[] group,
17 | string[] groupOwner,
18 | string[] nodeOwner,
19 | out string username,
20 | [NotNullWhen(true)] out User? user,
21 | [NotNullWhen(true)] out string[]? targetGroupOwnerIds,
22 | [NotNullWhen(true)] out string[]? targetNodeOwnerIds,
23 | [NotNullWhen(false)] out ObjectResult? objectResult)
24 | {
25 | username = "";
26 | user = null;
27 | targetGroupOwnerIds = null;
28 | targetNodeOwnerIds = null;
29 | objectResult = null;
30 |
31 | if (!dataService.UsersData.TryGetUserById(id, out var userEntry))
32 | {
33 | objectResult = NotFound($"User ID {id} doesn't exist.");
34 | return false;
35 | }
36 |
37 | username = userEntry.Key;
38 | user = userEntry.Value;
39 |
40 | LogRequest(username, id, HeaderHelper.GetRealIP(HttpContext), HttpContext.Request.Query);
41 |
42 | var validGroups = group.All(x => dataService.NodesData.Groups.ContainsKey(x));
43 | if (!validGroups)
44 | {
45 | objectResult = BadRequest("Not all groups exist.");
46 | return false;
47 | }
48 |
49 | var validGroupOwners = FilterHelper.TryGetUserIds(dataService.UsersData, groupOwner, out targetGroupOwnerIds);
50 | if (!validGroupOwners)
51 | {
52 | objectResult = BadRequest("Not all group owners exist.");
53 | return false;
54 | }
55 |
56 | var validNodeOwners = FilterHelper.TryGetUserIds(dataService.UsersData, nodeOwner, out targetNodeOwnerIds);
57 | if (!validNodeOwners)
58 | {
59 | objectResult = BadRequest("Not all node owners exist.");
60 | return false;
61 | }
62 |
63 | return true;
64 | }
65 |
66 | [LoggerMessage(Level = LogLevel.Information, Message = "{Username} ({Id}) retrieved online config from {Ip} with query {Query}")]
67 | private partial void LogRequest(string username, string id, IPAddress? ip, IQueryCollection query);
68 | }
69 |
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator.Server/Controllers/SIP008Controller.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Mvc;
2 | using ShadowsocksUriGenerator.OnlineConfig;
3 | using ShadowsocksUriGenerator.Server.Utils;
4 | using ShadowsocksUriGenerator.Services;
5 |
6 | namespace ShadowsocksUriGenerator.Server.Controllers;
7 |
8 | [ApiController]
9 | [ApiConventionType(typeof(DefaultApiConventions))]
10 | [Route("sip008")]
11 | public class SIP008Controller(ILogger logger, IDataService dataService) : OnlineConfigControllerBase(logger, dataService)
12 | {
13 |
14 | ///
15 | /// Gets online config by user ID in SIP008 format.
16 | ///
17 | ///
18 | /// Returns the online config document.
19 | ///
20 | /// GET /[secret]/sip008/[user_id]
21 | /// {
22 | /// "version": 1,
23 | /// "username": "database64128",
24 | /// "id": "[user_id]",
25 | /// "bytes_used": 52940262597,
26 | /// "bytes_remaining": 52940262597,
27 | /// "servers": [
28 | /// {
29 | /// "id": "27b8a625-4f4b-4428-9f0f-8a2317db7c79",
30 | /// "remarks": "ServerName",
31 | /// "owner": "database64128",
32 | /// "group": "examples",
33 | /// "tags": [ "direct" ],
34 | /// "server": "example.com",
35 | /// "server_port": 8388,
36 | /// "method": "2022-blake3-aes-256-gcm",
37 | /// "password": "z7by/oMFjG7sunqq2q69hlGynqkrgk9bCKoWp29zhgw=",
38 | /// "plugin": "plugin-name",
39 | /// "plugin_version": "1.0",
40 | /// "plugin_opts": "whatever",
41 | /// "plugin_args": "-vvvvvv"
42 | /// }
43 | /// ]
44 | /// }
45 | ///
46 | ///
47 | /// User ID.
48 | /// Select nodes that contain all tags in this array.
49 | /// Select nodes from groups in this array.
50 | /// Select nodes from groups that belong to users in this array.
51 | /// Select nodes that belong to users in this array.
52 | /// Whether to sort nodes by name. Defaults to false, or no sorting.
53 | /// The online config document in SIP008 format.
54 | /// Returns the online config document.
55 | /// One or more queries contain invalid values.
56 | /// The provided user ID doesn't exist.
57 | [HttpGet("{id}")]
58 | [ProducesResponseType(StatusCodes.Status200OK)]
59 | [ProducesResponseType(StatusCodes.Status400BadRequest)]
60 | [ProducesResponseType(StatusCodes.Status404NotFound)]
61 | public ActionResult GetByUserId(string id, [FromQuery] string[] tag, [FromQuery] string[] group, [FromQuery] string[] groupOwner, [FromQuery] string[] nodeOwner, [FromQuery] bool sortByName)
62 | {
63 | if (!TryGetUserEntry(id, group, groupOwner, nodeOwner, out var username, out var user, out var targetGroupOwnerIds, out var targetNodeOwnerIds, out var objectResult))
64 | return objectResult;
65 |
66 | var servers = user.GetShadowsocksServers(DataService.UsersData, DataService.NodesData, group, tag, targetGroupOwnerIds, targetNodeOwnerIds);
67 |
68 | if (sortByName)
69 | servers = servers.OrderBy(x => x.Name);
70 |
71 | var resp = new SIP008Config()
72 | {
73 | Username = username,
74 | Id = id,
75 | BytesUsed = user.BytesUsed > 0UL ? user.BytesUsed : null,
76 | BytesRemaining = user.BytesRemaining > 0UL ? user.BytesRemaining : null,
77 | Servers = servers.Select(x => new SIP008Server(x)),
78 | };
79 |
80 | return new JsonResult(resp, JsonHelper.APISnakeCaseJsonSerializerOptions);
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator.Server/Controllers/ShadowsocksGoClientConfigController.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Mvc;
2 | using ShadowsocksUriGenerator.OnlineConfig;
3 | using ShadowsocksUriGenerator.Services;
4 |
5 | namespace ShadowsocksUriGenerator.Server.Controllers;
6 |
7 | [ApiController]
8 | [ApiConventionType(typeof(DefaultApiConventions))]
9 | [Route("shadowsocks-go/clients")]
10 | public class ShadowsocksGoClientConfigController(ILogger logger, IDataService dataService) : OnlineConfigControllerBase(logger, dataService)
11 | {
12 |
13 | ///
14 | /// Gets online config by user ID in shadowsocks-go client config format.
15 | ///
16 | ///
17 | /// Returns the online config document.
18 | ///
19 | /// GET /[secret]/shadowsocks-go/clients/[user_id]
20 | /// {
21 | /// "clients": [
22 | /// {
23 | /// "name": "ServerName",
24 | /// "endpoint": "[2001:db8:bd63:362c:2071:a0f6:827:ab6a]:20220",
25 | /// "protocol": "2022-blake3-aes-128-gcm",
26 | /// "dialerFwmark": 0,
27 | /// "dialerTrafficClass": 0,
28 | /// "enableTCP": true,
29 | /// "dialerTFO": true,
30 | /// "enableUDP": true,
31 | /// "mtu": 1500,
32 | /// "psk": "qQln3GlVCZi5iJUObJVNCw==",
33 | /// "iPSKs": [
34 | /// "oE/s2z9Q8EWORAB8B3UCxw=="
35 | /// ],
36 | /// "paddingPolicy": ""
37 | /// }
38 | /// ]
39 | /// }
40 | ///
41 | ///
42 | /// User ID.
43 | /// Select nodes that contain all tags in this array.
44 | /// Select nodes from groups in this array.
45 | /// Select nodes from groups that belong to users in this array.
46 | /// Select nodes that belong to users in this array.
47 | /// The padding policy to use for outgoing Shadowsocks traffic.
48 | /// Whether to sort nodes by name. Defaults to false, or no sorting.
49 | ///
50 | /// If set to true, the generated clients won't include a direct client.
51 | /// By default a direct client is added since this is most certainly what the user wants.
52 | ///
53 | /// Whether to disable TCP for servers.
54 | /// Whether to disable TCP Fast Open for servers.
55 | /// Whether to disable UDP for servers.
56 | /// Set a fwmark for sockets.
57 | /// Set a traffic class for sockets.
58 | /// The path MTU between client and server. Defaults to 1492 for PPPoE.
59 | /// The online config document in shadowsocks-go client config format.
60 | /// Returns the online config document.
61 | /// One or more queries contain invalid values.
62 | /// The provided user ID doesn't exist.
63 | [HttpGet("{id}")]
64 | [ProducesResponseType(StatusCodes.Status200OK)]
65 | [ProducesResponseType(StatusCodes.Status400BadRequest)]
66 | [ProducesResponseType(StatusCodes.Status404NotFound)]
67 | public ActionResult GetByUserId(
68 | string id,
69 | [FromQuery] string[] tag,
70 | [FromQuery] string[] group,
71 | [FromQuery] string[] groupOwner,
72 | [FromQuery] string[] nodeOwner,
73 | [FromQuery] string? paddingPolicy,
74 | [FromQuery] bool sortByName,
75 | [FromQuery] bool noDirect,
76 | [FromQuery] bool disableTCP,
77 | [FromQuery] bool disableTFO,
78 | [FromQuery] bool disableUDP,
79 | [FromQuery] int dialerFwmark,
80 | [FromQuery] int dialerTrafficClass,
81 | [FromQuery] int mtu = 1492)
82 | {
83 | if (!TryGetUserEntry(id, group, groupOwner, nodeOwner, out var username, out var user, out var targetGroupOwnerIds, out var targetNodeOwnerIds, out var objectResult))
84 | return objectResult;
85 |
86 | var servers = user.GetShadowsocksServers(DataService.UsersData, DataService.NodesData, group, tag, targetGroupOwnerIds, targetNodeOwnerIds);
87 |
88 | if (sortByName)
89 | servers = servers.OrderBy(x => x.Name);
90 |
91 | var clients = servers.Select(x => new ShadowsocksGoClientConfig(x, paddingPolicy, disableTCP, disableTFO, disableUDP, dialerFwmark, dialerTrafficClass, mtu));
92 |
93 | if (!noDirect)
94 | clients = clients.Append(new(disableTCP, disableTFO, disableUDP, dialerFwmark, dialerTrafficClass, mtu));
95 |
96 | return new ShadowsocksGoConfig()
97 | {
98 | Clients = clients,
99 | };
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator.Server/Controllers/SingBoxOutboundConfigController.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Mvc;
2 | using ShadowsocksUriGenerator.OnlineConfig;
3 | using ShadowsocksUriGenerator.Server.Utils;
4 | using ShadowsocksUriGenerator.Services;
5 |
6 | namespace ShadowsocksUriGenerator.Server.Controllers;
7 |
8 | [ApiController]
9 | [ApiConventionType(typeof(DefaultApiConventions))]
10 | [Route("sing-box/outbounds")]
11 | public class SingBoxOutboundConfigController(ILogger logger, IDataService dataService) : OnlineConfigControllerBase(logger, dataService)
12 | {
13 |
14 | ///
15 | /// Gets online config by user ID in sing-box outbound config format.
16 | /// Visit https://sing-box.sagernet.org/ for documentation.
17 | ///
18 | ///
19 | /// Returns the online config document.
20 | ///
21 | /// GET /[secret]/sing-box/outbounds/[user_id]
22 | /// {
23 | /// "outbounds": [
24 | /// {
25 | /// "type": "shadowsocks",
26 | /// "tag": "ServerName",
27 | /// "server": "example.com",
28 | /// "server_port": 8388,
29 | /// "method": "2022-blake3-aes-256-gcm",
30 | /// "password": "z7by/oMFjG7sunqq2q69hlGynqkrgk9bCKoWp29zhgw="
31 | /// }
32 | /// ]
33 | /// }
34 | ///
35 | ///
36 | /// User ID.
37 | /// Select nodes that contain all tags in this array.
38 | /// Select nodes from groups in this array.
39 | /// Select nodes from groups that belong to users in this array.
40 | /// Select nodes that belong to users in this array.
41 | /// Whether to sort nodes by name. Defaults to false, or no sorting.
42 | ///
43 | /// If set to true, the generated outbound config won't include a selector.
44 | /// By default a selector with all servers is added.
45 | ///
46 | /// The selector's outbound tag. If unspecified, defaults to "default".
47 | /// The selector's default outbound. If unspecified, defaults to the first server.
48 | /// Either "tcp" or "udp". If unspecified, both are enabled.
49 | /// Whether to enable UDP-over-TCP.
50 | /// Whether to enable multiplexing.
51 | /// Select a multiplexing protocol.
52 | /// Max number of connections.
53 | /// Min number of streams.
54 | /// Max number of streams.
55 | /// Select an upstream outbound.
56 | /// The network interface to bind to.
57 | /// The IPv4 address to bind to.
58 | /// The IPv6 address to bind to.
59 | /// Set a fwmark on sockets.
60 | /// Whether to set SO_REUSEADDR.
61 | /// The connect timeout in Go's duration string format.
62 | /// Whether to enable TCP fast open.
63 | /// Whether to enable UDP fragmentation.
64 | /// One of prefer_ipv4 prefer_ipv6 ipv4_only ipv6_only.
65 | /// Happy Eyeballs fallback delay.
66 | /// The online config document in sing-box config format.
67 | /// Returns the online config document.
68 | /// One or more queries contain invalid values.
69 | /// The provided user ID doesn't exist.
70 | [HttpGet("{id}")]
71 | [ProducesResponseType(StatusCodes.Status200OK)]
72 | [ProducesResponseType(StatusCodes.Status400BadRequest)]
73 | [ProducesResponseType(StatusCodes.Status404NotFound)]
74 | public ActionResult GetByUserId(
75 | string id,
76 | [FromQuery] string[] tag,
77 | [FromQuery] string[] group,
78 | [FromQuery] string[] groupOwner,
79 | [FromQuery] string[] nodeOwner,
80 | [FromQuery] bool sortByName,
81 | [FromQuery] bool noSelector,
82 | [FromQuery] string? selectorTag,
83 | [FromQuery] string? selectorDefault,
84 | [FromQuery] string? network,
85 | [FromQuery] bool uot,
86 | [FromQuery] bool multiplex,
87 | [FromQuery] string? multiplexProtocol,
88 | [FromQuery] int multiplexMaxConnections,
89 | [FromQuery] int multiplexMinStreams,
90 | [FromQuery] int multiplexMaxStreams,
91 | [FromQuery] string? detour,
92 | [FromQuery] string? bindInterface,
93 | [FromQuery] string? inet4BindAddress,
94 | [FromQuery] string? inet6BindAddress,
95 | [FromQuery] int routingMark,
96 | [FromQuery] bool reuseAddr,
97 | [FromQuery] string? connectTimeout,
98 | [FromQuery] bool tcpFastOpen,
99 | [FromQuery] bool udpFragment,
100 | [FromQuery] string? domainStrategy,
101 | [FromQuery] string? fallbackDelay)
102 | {
103 | if (!TryGetUserEntry(id, group, groupOwner, nodeOwner, out var username, out var user, out var targetGroupOwnerIds, out var targetNodeOwnerIds, out var objectResult))
104 | return objectResult;
105 |
106 | var servers = user.GetShadowsocksServers(DataService.UsersData, DataService.NodesData, group, tag, targetGroupOwnerIds, targetNodeOwnerIds);
107 |
108 | if (sortByName)
109 | servers = servers.OrderBy(x => x.Name);
110 |
111 | var outbounds = servers.Select(x => new SingBoxOutboundConfig(x, network, uot, multiplex, multiplexProtocol, multiplexMaxConnections, multiplexMinStreams, multiplexMaxStreams, detour, bindInterface, inet4BindAddress, inet6BindAddress, routingMark, reuseAddr, connectTimeout, tcpFastOpen, udpFragment, domainStrategy, fallbackDelay));
112 |
113 | if (!noSelector && servers.Any())
114 | {
115 | outbounds = outbounds.Append(new()
116 | {
117 | Type = "selector",
118 | Tag = selectorTag ?? "default",
119 | Outbounds = servers.Select(x => x.Name),
120 | Default = selectorDefault ?? servers.First().Name,
121 | });
122 | }
123 |
124 | var resp = new SingBoxConfig()
125 | {
126 | Outbounds = outbounds,
127 | };
128 |
129 | return new JsonResult(resp, JsonHelper.APISnakeCaseJsonSerializerOptions);
130 | }
131 | }
132 |
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator.Server/Program.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.HttpOverrides;
2 | using Microsoft.OpenApi.Models;
3 | using ShadowsocksUriGenerator.OnlineConfig;
4 | using ShadowsocksUriGenerator.Services;
5 | using System.Text.Encodings.Web;
6 | using System.Text.Json.Serialization;
7 |
8 | var builder = WebApplication.CreateBuilder(args);
9 |
10 | builder.Services.AddSingleton();
11 | builder.Services.AddHostedService(provider => provider.GetService() as DataService ?? throw new Exception("Injected IDataService is not DataService."));
12 | builder.Services.AddControllers()
13 | .AddJsonOptions(options =>
14 | {
15 | options.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault;
16 | options.JsonSerializerOptions.Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping;
17 | options.JsonSerializerOptions.IgnoreReadOnlyProperties = true;
18 |
19 | // STJ source generation only works with minimal API.
20 | // So the following lines effectively do nothing right now.
21 | options.JsonSerializerOptions.TypeInfoResolverChain.Add(OnlineConfigCamelCaseJsonSerializerContext.Default);
22 | options.JsonSerializerOptions.TypeInfoResolverChain.Add(OnlineConfigSnakeCaseJsonSerializerContext.Default);
23 | });
24 |
25 | builder.Services.AddSwaggerGen(c =>
26 | {
27 | c.SwaggerDoc("v1", new OpenApiInfo
28 | {
29 | Title = "Shadowsocks URI Generator API Server",
30 | Description = "Shadowsocks URI Generator API Specifications",
31 | Version = "v1",
32 | });
33 |
34 | var xmlPath = $"{AppContext.BaseDirectory}{Path.GetFileNameWithoutExtension(Environment.GetCommandLineArgs()[0])}.xml";
35 |
36 | if (File.Exists(xmlPath))
37 | {
38 | c.IncludeXmlComments(xmlPath);
39 | }
40 | else
41 | {
42 | Console.WriteLine("Warning: XML comment file not found.");
43 | }
44 | });
45 |
46 | builder.Services.Configure(options =>
47 | {
48 | options.ForwardedHeaders = ForwardedHeaders.All;
49 | options.ForwardLimit = null;
50 | options.KnownProxies.Clear();
51 | options.KnownNetworks.Clear();
52 | });
53 |
54 | var app = builder.Build();
55 |
56 | app.UseSwagger();
57 | app.UseSwaggerUI();
58 |
59 | app.UseReDoc();
60 |
61 | app.UseForwardedHeaders();
62 |
63 | app.UseAuthorization();
64 |
65 | app.MapControllers();
66 |
67 | await app.RunAsync();
68 |
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator.Server/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "http://json.schemastore.org/launchsettings.json",
3 | "iisSettings": {
4 | "windowsAuthentication": false,
5 | "anonymousAuthentication": true,
6 | "iisExpress": {
7 | "applicationUrl": "http://localhost:51936",
8 | "sslPort": 0
9 | }
10 | },
11 | "profiles": {
12 | "IIS Express": {
13 | "commandName": "IISExpress",
14 | "launchBrowser": true,
15 | "launchUrl": "swagger",
16 | "environmentVariables": {
17 | "ASPNETCORE_ENVIRONMENT": "Development"
18 | }
19 | },
20 | "ShadowsocksUriGenerator.Server": {
21 | "commandName": "Project",
22 | "dotnetRunMessages": "true",
23 | "launchBrowser": true,
24 | "launchUrl": "swagger",
25 | "applicationUrl": "http://localhost:5000",
26 | "environmentVariables": {
27 | "ASPNETCORE_ENVIRONMENT": "Development"
28 | }
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator.Server/ShadowsocksUriGenerator.Server.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net9.0
5 | enable
6 | enable
7 | ss-uri-gen-server
8 | ShadowsocksUriGenerator.Server
9 | 5.2.0
10 | database64128
11 | Shadowsocks URI Generator API Server
12 | Shadowsocks URI Generator API Server provides an API endpoint for basic management tasks and online config.
13 | © 2023 database64128
14 | LICENSE
15 | https://github.com/database64128/shadowsocks-uri-generator
16 | https://github.com/database64128/shadowsocks-uri-generator
17 | Public
18 | rest-api;online-config;open-online-config
19 | true
20 | false
21 | CS1591
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | True
36 |
37 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator.Server/Utils/FilterHelper.cs:
--------------------------------------------------------------------------------
1 | using ShadowsocksUriGenerator.Data;
2 |
3 | namespace ShadowsocksUriGenerator.Server.Utils
4 | {
5 | public static class FilterHelper
6 | {
7 | ///
8 | /// Attempts to resolve the array of usernames
9 | /// into an array of user IDs.
10 | ///
11 | /// The object.
12 | /// The array of usernames to resolve.
13 | /// The resolved user IDs. Contains all resolved user IDs, no matter the return value.
14 | ///
15 | /// True if all usernames are successfully resolved.
16 | /// Otherwise false.
17 | ///
18 | public static bool TryGetUserIds(Users users, string[] usernames, out string[] ids)
19 | {
20 | var result = true;
21 | var idList = new List();
22 |
23 | foreach (var username in usernames)
24 | {
25 | if (users.UserDict.TryGetValue(username, out var user))
26 | {
27 | idList.Add(user.Uuid);
28 | }
29 | else
30 | {
31 | result = false;
32 | }
33 | }
34 |
35 | ids = [.. idList];
36 | return result;
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator.Server/Utils/HeaderHelper.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.Primitives;
2 | using System.Net;
3 |
4 | namespace ShadowsocksUriGenerator.Server.Utils
5 | {
6 | public static class HeaderHelper
7 | {
8 | public static IPAddress? GetRealIP(HttpContext ctx)
9 | {
10 | var xRealIpHeader = ctx.Request.Headers["X-Real-IP"];
11 |
12 | if (!StringValues.IsNullOrEmpty(xRealIpHeader) && IPAddress.TryParse(xRealIpHeader, out var ip))
13 | return ip;
14 | else
15 | return ctx.Connection.RemoteIpAddress;
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator.Server/Utils/JsonHelper.cs:
--------------------------------------------------------------------------------
1 | using System.Text.Encodings.Web;
2 | using System.Text.Json;
3 | using System.Text.Json.Serialization;
4 |
5 | namespace ShadowsocksUriGenerator.Server.Utils;
6 |
7 | public class JsonHelper
8 | {
9 | public static readonly JsonSerializerOptions APISnakeCaseJsonSerializerOptions = new()
10 | {
11 | DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault,
12 | Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
13 | IgnoreReadOnlyProperties = true,
14 | PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
15 | };
16 | }
17 |
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator.Server/appsettings.Development.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft.AspNetCore": "Warning"
6 | }
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator.Server/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft.AspNetCore": "Warning"
6 | }
7 | },
8 | "AllowedHosts": "*"
9 | }
10 |
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator.Server/appsettings.systemd.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft.AspNetCore": "Warning"
6 | },
7 | "Console": {
8 | "FormatterName": "systemd"
9 | }
10 | },
11 | "AllowedHosts": "*"
12 | }
13 |
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator.Services/IDataService.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.Hosting;
2 | using ShadowsocksUriGenerator.Data;
3 |
4 | namespace ShadowsocksUriGenerator.Services
5 | {
6 | public interface IDataService : IHostedService
7 | {
8 | public Users UsersData { get; }
9 | public Nodes NodesData { get; }
10 | public Settings SettingsData { get; }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator.Services/ShadowsocksUriGenerator.Services.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net9.0
5 | enable
6 | enable
7 | true
8 | 5.2.0
9 | database64128
10 | Shadowsocks URI Generator Hosted Services
11 | Shadowsocks URI Generator hosted services.
12 | © 2023 database64128
13 | LICENSE
14 | https://github.com/database64128/shadowsocks-uri-generator
15 | https://github.com/database64128/shadowsocks-uri-generator
16 | Public
17 | false
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | True
32 |
33 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator.Tests/ShadowsocksUriGenerator.Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net9.0
5 |
6 | false
7 |
8 | enable
9 | enable
10 |
11 |
12 |
13 |
14 |
15 |
16 | runtime; build; native; contentfiles; analyzers; buildtransitive
17 | all
18 |
19 |
20 | runtime; build; native; contentfiles; analyzers; buildtransitive
21 | all
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator.Tests/UtilTests.cs:
--------------------------------------------------------------------------------
1 | using ShadowsocksUriGenerator.Utils;
2 | using Xunit;
3 |
4 | namespace ShadowsocksUriGenerator.Tests
5 | {
6 | public class UtilTests
7 | {
8 | [Theory]
9 | [InlineData("0", true, 0UL)]
10 | [InlineData("1024", true, 1024UL)]
11 | [InlineData("2K", true, 2048UL)]
12 | [InlineData("4M", true, 4194304UL)]
13 | [InlineData("8G", true, 8589934592UL)]
14 | [InlineData("16T", true, 17592186044416UL)]
15 | [InlineData("32P", true, 36028797018963968UL)]
16 | [InlineData("8E", true, 9223372036854775808UL)]
17 | [InlineData("", false, 0UL)]
18 | [InlineData("M", false, 0UL)]
19 | [InlineData("64B", false, 0UL)]
20 | [InlineData("128g", false, 0UL)]
21 | [InlineData("BYTE", false, 0UL)]
22 | [InlineData("32,768", false, 0UL)]
23 | [InlineData("65535MEM", false, 0UL)]
24 | public void Parse_DataLimitString_ReturnsBoolUlong(string dataLimitString, bool expectedResult, ulong expectedDataLimit)
25 | {
26 | var parseResult = InteractionHelper.TryParseDataLimitString(dataLimitString, out var parsedDataLimit);
27 |
28 | Assert.Equal(expectedResult, parseResult);
29 | Assert.Equal(expectedDataLimit, parsedDataLimit);
30 | }
31 |
32 | [Theory]
33 | [InlineData(0UL, false, false, "0")]
34 | [InlineData(0UL, false, true, "0 B")]
35 | [InlineData(0UL, true, false, "0")]
36 | [InlineData(0UL, true, true, "0 B")]
37 | [InlineData(1024UL, false, false, "1.024 K")]
38 | [InlineData(2048UL, false, true, "2.048 KB")]
39 | [InlineData(2560UL, true, false, "2.5 Ki")]
40 | [InlineData(4096UL, true, true, "4 KiB")]
41 | [InlineData(4194304UL, false, false, "4.194 M")]
42 | [InlineData(6291456UL, false, true, "6.291 MB")]
43 | [InlineData(8388608UL, true, false, "8 Mi")]
44 | [InlineData(536870912UL, true, true, "512 MiB")]
45 | [InlineData(1073741824UL, false, false, "1.074 G")]
46 | [InlineData(137438953472UL, false, true, "137.4 GB")]
47 | [InlineData(137975824384UL, true, false, "128.5 Gi")]
48 | [InlineData(1098437885952UL, true, true, "1023 GiB")]
49 | [InlineData(1099511627776UL, false, false, "1.1 T")]
50 | [InlineData(140737488355328UL, false, true, "140.7 TB")]
51 | [InlineData(281474976710656UL, true, false, "256 Ti")]
52 | [InlineData(1124800395214848UL, true, true, "1023 TiB")]
53 | [InlineData(1125899906842624UL, false, false, "1.126 P")]
54 | [InlineData(144115188075855872UL, false, true, "144.1 PB")]
55 | [InlineData(288230376151711744UL, true, false, "256 Pi")]
56 | [InlineData(1151795604700004352UL, true, true, "1023 PiB")]
57 | [InlineData(1152921504606846976UL, false, false, "1.153 E")]
58 | [InlineData(2305843009213693952UL, false, true, "2.306 EB")]
59 | [InlineData(4611686018427387904UL, true, false, "4 Ei")]
60 | [InlineData(6917529027641081856UL, true, true, "6 EiB")]
61 | public void HumanReadableDataString_FromUlong_ToString(ulong dataInBytes, bool middle_i, bool trailingB, string expectedDataString)
62 | {
63 | var dataString = InteractionHelper.HumanReadableDataString(dataInBytes, middle_i, trailingB);
64 |
65 | Assert.Equal(expectedDataString, dataString);
66 | }
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator/Data/DataJsonSerializerContext.cs:
--------------------------------------------------------------------------------
1 | using System.Text.Json;
2 | using System.Text.Json.Serialization;
3 |
4 | namespace ShadowsocksUriGenerator.Data;
5 |
6 | [JsonSerializable(typeof(Users))]
7 | [JsonSerializable(typeof(Nodes))]
8 | [JsonSourceGenerationOptions(
9 | AllowTrailingCommas = true,
10 | DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault,
11 | IgnoreReadOnlyProperties = true,
12 | ReadCommentHandling = JsonCommentHandling.Skip,
13 | WriteIndented = true)]
14 | public partial class DataJsonSerializerContext : JsonSerializerContext
15 | {
16 | }
17 |
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator/Data/GroupApiRequestException.cs:
--------------------------------------------------------------------------------
1 | namespace ShadowsocksUriGenerator.Data;
2 |
3 | public class GroupApiRequestException : Exception
4 | {
5 | public GroupApiRequestException()
6 | { }
7 |
8 | public GroupApiRequestException(string? message) : base(message)
9 | { }
10 |
11 | public GroupApiRequestException(string? message, Exception? innerException) : base(message, innerException)
12 | { }
13 |
14 | public GroupApiRequestException(string? groupName, string? message, Exception? innerException) : base(message, innerException)
15 | {
16 | GroupName = groupName;
17 | }
18 |
19 | public override string Message
20 | {
21 | get
22 | {
23 | string s = base.Message;
24 | if (GroupName is not null)
25 | s += Environment.NewLine + "Group: " + GroupName;
26 | return s;
27 | }
28 | }
29 |
30 | ///
31 | /// Gets the name of the group for which the API request failed.
32 | ///
33 | public string? GroupName { get; }
34 | }
35 |
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator/Data/MemberInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Diagnostics.CodeAnalysis;
2 | using System.Security.Cryptography;
3 | using System.Text.Json.Serialization;
4 |
5 | namespace ShadowsocksUriGenerator.Data;
6 |
7 | ///
8 | /// Stores a user's credential and data limit in a group.
9 | ///
10 | public sealed class MemberInfo : IEquatable
11 | {
12 | public string Method { get; set; }
13 |
14 | public string Password { get; set; }
15 |
16 | ///
17 | /// Gets whether the member info contains credential.
18 | ///
19 | [JsonIgnore]
20 | public bool HasCredential => !string.IsNullOrEmpty(Method) && !string.IsNullOrEmpty(Password);
21 |
22 | ///
23 | /// Gets or sets the data limit in bytes
24 | /// enforced on the user in the group.
25 | /// Do not set this if it's a global or general per-key limit.
26 | /// Set this if the limit is specifically targeting this user in the group.
27 | ///
28 | public ulong DataLimitInBytes { get; set; }
29 |
30 | ///
31 | /// Gets or sets the data usage in bytes.
32 | ///
33 | public ulong BytesUsed { get; set; }
34 |
35 | ///
36 | /// Gets or sets the data remaining to be used in bytes.
37 | ///
38 | public ulong BytesRemaining { get; set; }
39 |
40 | ///
41 | /// Parameterless constructor for System.Text.Json
42 | ///
43 | public MemberInfo()
44 | {
45 | Method = "";
46 | Password = "";
47 | }
48 |
49 | ///
50 | /// Constructs a member info object with the given method and a generated password.
51 | ///
52 | /// Method name.
53 | public MemberInfo(string method)
54 | {
55 | Method = method;
56 | GeneratePassword();
57 | }
58 |
59 | public MemberInfo(string method, string password)
60 | {
61 | Method = method;
62 | Password = password;
63 | }
64 |
65 | public bool Equals(MemberInfo? other) => Method == other?.Method && Password == other?.Password;
66 |
67 | public override bool Equals(object? obj) => Equals(obj as MemberInfo);
68 |
69 | public override int GetHashCode() => Method.GetHashCode() ^ Password.GetHashCode();
70 |
71 | [MemberNotNull(nameof(Password))]
72 | public void GeneratePassword()
73 | {
74 | int keySize = Method switch
75 | {
76 | "2022-blake3-aes-128-gcm" => 16,
77 | _ => 32,
78 | };
79 | Span key = stackalloc byte[keySize];
80 | RandomNumberGenerator.Fill(key);
81 | Password = Convert.ToBase64String(key);
82 | }
83 |
84 | ///
85 | /// Clears the credential information.
86 | ///
87 | public void ClearCredential()
88 | {
89 | Method = "";
90 | Password = "";
91 | }
92 |
93 | public string PasswordForNode(List iPSKs)
94 | {
95 | if (iPSKs.Count == 0)
96 | return Password;
97 |
98 | var length = iPSKs.Count + iPSKs.Sum(x => x.Length) + Password.Length;
99 | return string.Create(length, iPSKs, (chars, iPSKs) =>
100 | {
101 | foreach (var iPSK in iPSKs)
102 | {
103 | iPSK.CopyTo(chars);
104 | chars[iPSK.Length] = ':';
105 | chars = chars[(iPSK.Length + 1)..];
106 | }
107 |
108 | Password.CopyTo(chars);
109 | });
110 | }
111 |
112 | public void AddBytesUsed(ulong bytesUsed, ulong perUserDataLimitInBytes)
113 | {
114 | BytesUsed += bytesUsed;
115 | UpdateBytesRemaining(perUserDataLimitInBytes);
116 | }
117 |
118 | public void SubBytesUsed(ulong bytesUsed, ulong perUserDataLimitInBytes)
119 | {
120 | BytesUsed = BytesUsed >= bytesUsed ? BytesUsed - bytesUsed : 0UL;
121 | UpdateBytesRemaining(perUserDataLimitInBytes);
122 | }
123 |
124 | public void ClearBytesUsed(ulong perUserDataLimitInBytes)
125 | {
126 | BytesUsed = 0UL;
127 | UpdateBytesRemaining(perUserDataLimitInBytes);
128 | }
129 |
130 | private void UpdateBytesRemaining(ulong perUserDataLimitInBytes)
131 | {
132 | ulong dataLimitInBytes = DataLimitInBytes > 0UL ? DataLimitInBytes : perUserDataLimitInBytes;
133 | BytesRemaining = dataLimitInBytes > BytesUsed ? dataLimitInBytes - BytesUsed : 0UL;
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator/Data/Node.cs:
--------------------------------------------------------------------------------
1 | namespace ShadowsocksUriGenerator.Data
2 | {
3 | ///
4 | /// Stores node's host and port.
5 | ///
6 | public sealed class Node
7 | {
8 | ///
9 | /// Gets or sets the UUID of the node.
10 | ///
11 | public string Uuid { get; set; } = Guid.NewGuid().ToString();
12 | public string Host { get; set; } = "";
13 | public int Port { get; set; }
14 | public string? Plugin { get; set; }
15 | public string? PluginVersion { get; set; }
16 | public string? PluginOpts { get; set; }
17 | public string? PluginArguments { get; set; }
18 |
19 | ///
20 | /// Gets or sets whether the node is deactivated.
21 | /// Defaults to false, or activated.
22 | /// When set to true, the node is excluded from delivery.
23 | ///
24 | public bool Deactivated { get; set; }
25 |
26 | ///
27 | /// Gets or sets the node's owner.
28 | ///
29 | public string? OwnerUuid { get; set; }
30 |
31 | ///
32 | /// Gets or sets the node's tags.
33 | ///
34 | public List Tags { get; set; } = [];
35 |
36 | ///
37 | /// Gets or sets the node's identity PSKs.
38 | ///
39 | public List IdentityPSKs { get; set; } = [];
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator/Federation/Config/FederatedPeerConfig.cs:
--------------------------------------------------------------------------------
1 | using ShadowsocksUriGenerator.OnlineConfig;
2 |
3 | namespace ShadowsocksUriGenerator.Federation.Config;
4 |
5 | public class FederatedPeerConfig
6 | {
7 | public ulong Id { get; set; }
8 | public string Name { get; set; } = "";
9 | public string Password { get; set; } = "";
10 |
11 | ///
12 | /// Gets or sets the peer's API endpoint.
13 | /// Leave this property null if the peer does not
14 | /// have a public API endpoint.
15 | ///
16 | public OOCv1ApiToken? ApiEndpoint { get; set; }
17 | }
18 |
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator/Federation/Config/FederationConfig.cs:
--------------------------------------------------------------------------------
1 | using ShadowsocksUriGenerator.Federation.Config.Shadowsocks;
2 |
3 | namespace ShadowsocksUriGenerator.Federation.Config;
4 |
5 | public class FederationConfig
6 | {
7 | ///
8 | /// Defines the default federation configuration version
9 | /// used by this version of the app.
10 | ///
11 | public static readonly int _defaultVersion = 1;
12 |
13 | ///
14 | /// Gets or sets the configuration version number.
15 | /// Update if older config is present.
16 | /// Throw error if config is newer than supported.
17 | ///
18 | public int Version { get; set; } = _defaultVersion;
19 |
20 | public Dictionary HostUsers { get; set; } = [];
21 |
22 | public Dictionary HostGroupsShadowsocksSimple { get; set; } = [];
23 |
24 | public Dictionary HostGroupsShadowsocksManager { get; set; } = [];
25 |
26 | public Dictionary HostGroupsOutline { get; set; } = [];
27 |
28 | public Dictionary FederatedPeers { get; set; } = [];
29 | }
30 |
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator/Federation/Config/HostUserConfig.cs:
--------------------------------------------------------------------------------
1 | namespace ShadowsocksUriGenerator.Federation.Config;
2 |
3 | public class HostUserConfig
4 | {
5 | public ulong Id { get; set; }
6 | public string Name { get; set; } = "";
7 | public string Password { get; set; } = "";
8 |
9 | ///
10 | /// Gets or sets the global data limit of the user in bytes.
11 | /// 0UL means no data limit.
12 | ///
13 | public ulong DataLimitInBytes { get; set; }
14 |
15 | ///
16 | /// Gets or sets the per-group data limit of the user in bytes.
17 | /// 0UL means no data limit.
18 | ///
19 | public ulong PerGroupDataLimitInBytes { get; set; }
20 | }
21 |
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator/Federation/Config/Shadowsocks/HostGroupConfigOutline.cs:
--------------------------------------------------------------------------------
1 | using ShadowsocksUriGenerator.Outline;
2 |
3 | namespace ShadowsocksUriGenerator.Federation.Config.Shadowsocks;
4 |
5 | public class HostGroupConfigOutline : HostGroupConfigShadowsocks
6 | {
7 | ///
8 | /// Gets or sets the Outline API key record.
9 | ///
10 | public required OutlineApiKey OutlineApiKey { get; set; }
11 | }
12 |
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator/Federation/Config/Shadowsocks/HostGroupConfigShadowsocks.cs:
--------------------------------------------------------------------------------
1 | using ShadowsocksUriGenerator.Federation.Protocols.Shadowsocks;
2 |
3 | namespace ShadowsocksUriGenerator.Federation.Config.Shadowsocks;
4 |
5 | public abstract class HostGroupConfigShadowsocks
6 | {
7 | public ulong Id { get; set; }
8 | public string Name { get; set; } = "";
9 |
10 | ///
11 | /// Gets or sets the group owner.
12 | ///
13 | public ulong OwnerId { get; set; }
14 |
15 | ///
16 | /// Stores all servers in this group.
17 | /// Key is server ID. Starts from 0.
18 | /// Value is server config.
19 | ///
20 | public Dictionary Servers { get; set; } = [];
21 | }
22 |
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator/Federation/Config/Shadowsocks/HostGroupConfigShadowsocksManager.cs:
--------------------------------------------------------------------------------
1 | namespace ShadowsocksUriGenerator.Federation.Config.Shadowsocks;
2 |
3 | public class HostGroupConfigShadowsocksManager : HostGroupConfigShadowsocks
4 | {
5 | public string? UnixDomainSocketPath { get; set; }
6 | public string? UDPHost { get; set; }
7 | public int UDPPort { get; set; }
8 |
9 | ///
10 | /// Gets or sets the minimum server port number.
11 | /// Use together with .
12 | /// Do not use with .
13 | ///
14 | public int MinServerPort { get; set; }
15 |
16 | ///
17 | /// Gets or sets the maximum server port number.
18 | /// Use together with .
19 | /// Do not use with .
20 | ///
21 | public int MaxServerPort { get; set; }
22 |
23 | ///
24 | /// Gets or sets a list of ports to be allocated for Shadowsocks server.
25 | /// Do not use with or .
26 | ///
27 | public List? ServerPortAllocationRange { get; set; }
28 | }
29 |
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator/Federation/Config/Shadowsocks/HostGroupConfigShadowsocksSimple.cs:
--------------------------------------------------------------------------------
1 | using ShadowsocksUriGenerator.Federation.Protocols.Shadowsocks;
2 |
3 | namespace ShadowsocksUriGenerator.Federation.Config.Shadowsocks;
4 |
5 | public class HostGroupConfigShadowsocksSimple : HostGroupConfigShadowsocks
6 | {
7 | ///
8 | /// Stores server credentials for all users.
9 | /// Key is user ID.
10 | /// Value is credential.
11 | ///
12 | public Dictionary Credentials { get; set; } = [];
13 | }
14 |
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator/Federation/Data/DataUsage.cs:
--------------------------------------------------------------------------------
1 | namespace ShadowsocksUriGenerator.Federation.Data;
2 |
3 | public class DataUsage
4 | {
5 | ///
6 | /// Gets or sets the data usage in bytes.
7 | ///
8 | public ulong BytesUsed { get; set; }
9 |
10 | ///
11 | /// Gets or sets the data remaining to be used in bytes.
12 | /// 0UL means used up.
13 | /// null means no data limit.
14 | ///
15 | public ulong? BytesRemaining { get; set; }
16 | }
17 |
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator/Federation/Data/FederatedPeerData.cs:
--------------------------------------------------------------------------------
1 | using ShadowsocksUriGenerator.Federation.Protocols.Shadowsocks;
2 | using ShadowsocksUriGenerator.OnlineConfig;
3 |
4 | namespace ShadowsocksUriGenerator.Federation.Data;
5 |
6 | public class FederatedPeerData
7 | {
8 | ///
9 | /// Gets or sets the peer's API endpoint.
10 | /// Updated by peer via OOCv1 federation API.
11 | /// Overrides the one defined in config.
12 | ///
13 | public OOCv1ApiToken? ApiEndpoint { get; set; }
14 |
15 | ///
16 | /// Stores all servers from this peer.
17 | /// Key is server ID. Starts from 0.
18 | /// Value is server config.
19 | ///
20 | public Dictionary Servers { get; set; } = [];
21 | }
22 |
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator/Federation/Data/FederationData.cs:
--------------------------------------------------------------------------------
1 | using ShadowsocksUriGenerator.Federation.Data.Shadowsocks;
2 |
3 | namespace ShadowsocksUriGenerator.Federation.Data;
4 |
5 | public class FederationData
6 | {
7 | ///
8 | /// Defines the default federation data format version
9 | /// used by this version of the app.
10 | ///
11 | public static readonly int _defaultVersion = 1;
12 |
13 | ///
14 | /// Gets or sets the data format version number.
15 | /// Update if older data format is present.
16 | /// Throw error if data format is newer than supported.
17 | ///
18 | public int Version { get; set; } = _defaultVersion;
19 |
20 | public Dictionary HostUsers { get; set; } = [];
21 |
22 | public Dictionary HostGroupsShadowsocksManager { get; set; } = [];
23 |
24 | public Dictionary HostGroupsOutline { get; set; } = [];
25 |
26 | public Dictionary FederatedPeers { get; set; } = [];
27 | }
28 |
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator/Federation/Data/HostUserData.cs:
--------------------------------------------------------------------------------
1 | namespace ShadowsocksUriGenerator.Federation.Data;
2 |
3 | public class HostUserData
4 | {
5 | ///
6 | /// Gets or sets the user's total data usage stats.
7 | ///
8 | public DataUsage TotalDataUsage { get; set; } = new();
9 | }
10 |
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator/Federation/Data/PeerUserData.cs:
--------------------------------------------------------------------------------
1 | using ShadowsocksUriGenerator.Federation.Protocols.Shadowsocks;
2 |
3 | namespace ShadowsocksUriGenerator.Federation.Data;
4 |
5 | public class PeerUserData
6 | {
7 | public ulong Id { get; set; }
8 | public string Name { get; set; } = "";
9 |
10 | ///
11 | /// Stores user credentials for all host groups.
12 | /// Key is host group ID.
13 | /// Value is credential.
14 | ///
15 | public Dictionary Credentials { get; set; } = [];
16 |
17 | ///
18 | /// Stores user data usage stats for all host groups.
19 | /// Key is host group ID.
20 | /// Value is data usage.
21 | ///
22 | public Dictionary DataUsageStats { get; set; } = [];
23 | }
24 |
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator/Federation/Data/Shadowsocks/HostGroupDataOutline.cs:
--------------------------------------------------------------------------------
1 | using ShadowsocksUriGenerator.Outline;
2 |
3 | namespace ShadowsocksUriGenerator.Federation.Data.Shadowsocks;
4 |
5 | public class HostGroupDataOutline : HostGroupDataShadowsocks
6 | {
7 | ///
8 | /// Gets or sets the Outline server information object.
9 | ///
10 | public OutlineServerInfo OutlineServerInfo { get; set; } = new();
11 |
12 | ///
13 | /// Gets or sets the dictionary
14 | /// that stores user ID to access key ID mappings.
15 | /// Key is user ID.
16 | /// Value is access key ID.
17 | ///
18 | public Dictionary UserAccessKeyDict { get; set; } = [];
19 |
20 | ///
21 | /// Gets or sets the per-user data limit of the group in bytes.
22 | /// 0UL means no data limit.
23 | ///
24 | public ulong PerUserDataLimitInBytes { get; set; }
25 |
26 | ///
27 | /// Gets or sets the dictionary
28 | /// that stores all users' data limit.
29 | /// Key is user ID.
30 | /// Value is data limit.
31 | /// Value 0UL means no data limit.
32 | ///
33 | public Dictionary UserDataLimitDict { get; set; } = [];
34 | }
35 |
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator/Federation/Data/Shadowsocks/HostGroupDataShadowsocks.cs:
--------------------------------------------------------------------------------
1 | using ShadowsocksUriGenerator.Federation.Protocols.Shadowsocks;
2 |
3 | namespace ShadowsocksUriGenerator.Federation.Data.Shadowsocks;
4 |
5 | public class HostGroupDataShadowsocks
6 | {
7 | ///
8 | /// Stores server credentials for all users.
9 | /// Key is user ID.
10 | /// Value is credential.
11 | ///
12 | public Dictionary Credentials { get; set; } = [];
13 |
14 | ///
15 | /// Gets or sets the group's total data usage stats.
16 | ///
17 | public DataUsage TotalDataUsage { get; set; } = new();
18 |
19 | ///
20 | /// Gets or sets data usage stats of all users.
21 | ///
22 | public Dictionary UserDataUsageStats { get; set; } = [];
23 | }
24 |
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator/Federation/Data/Shadowsocks/HostGroupDataShadowsocksManager.cs:
--------------------------------------------------------------------------------
1 | namespace ShadowsocksUriGenerator.Federation.Data.Shadowsocks;
2 |
3 | public class HostGroupDataShadowsocksManager : HostGroupDataShadowsocks
4 | {
5 | }
6 |
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator/Federation/Protocols/Shadowsocks/FederatedShadowsocksServerConfig.cs:
--------------------------------------------------------------------------------
1 | namespace ShadowsocksUriGenerator.Federation.Protocols.Shadowsocks;
2 |
3 | public class FederatedShadowsocksServerConfig
4 | {
5 | public string Name { get; set; } = "";
6 |
7 | public string Host { get; set; } = "";
8 |
9 | public int Port { get; set; }
10 |
11 | public string? Method { get; set; }
12 |
13 | public string? Password { get; set; }
14 |
15 | public string? PluginName { get; set; }
16 |
17 | public string? PluginVersion { get; set; }
18 |
19 | public string? PluginOptions { get; set; }
20 |
21 | public string? PluginArguments { get; set; }
22 |
23 | ///
24 | /// Gets or sets the owner of the server.
25 | ///
26 | public string? Owner { get; set; }
27 |
28 | ///
29 | /// Gets or sets the list of annotated tags.
30 | ///
31 | public List Tags { get; set; } = [];
32 | }
33 |
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator/Federation/Protocols/Shadowsocks/FederatedShadowsocksServerCredential.cs:
--------------------------------------------------------------------------------
1 | namespace ShadowsocksUriGenerator.Federation.Protocols.Shadowsocks;
2 |
3 | public class FederatedShadowsocksServerCredential
4 | {
5 | public int Port { get; set; }
6 |
7 | public string? Method { get; set; }
8 |
9 | public string? Password { get; set; }
10 |
11 | public string? PluginOptions { get; set; }
12 |
13 | public string? PluginArguments { get; set; }
14 | }
15 |
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator/Manager/IManagerApiClient.cs:
--------------------------------------------------------------------------------
1 | using ShadowsocksUriGenerator.OnlineConfig;
2 |
3 | namespace ShadowsocksUriGenerator.Manager;
4 |
5 | public interface IManagerApiClient
6 | {
7 | ManagerApiResponse Add(ManagerApiAddRequest request);
8 | ManagerApiResponse Remove(ManagerApiRemoveRequest request);
9 | SIP008Config List();
10 | Dictionary Ping();
11 | Dictionary Stat(Dictionary request);
12 |
13 | Task AddAsync(ManagerApiAddRequest request, CancellationToken cancellationToken = default);
14 | Task RemoveAsync(ManagerApiRemoveRequest request, CancellationToken cancellationToken = default);
15 | Task ListAsync(CancellationToken cancellationToken = default);
16 | Task> PingAsync(CancellationToken cancellationToken = default);
17 | Task> StatAsync(Dictionary request, CancellationToken cancellationToken = default);
18 | }
19 |
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator/Manager/IManagerApiTransport.cs:
--------------------------------------------------------------------------------
1 | namespace ShadowsocksUriGenerator.Manager;
2 |
3 | internal interface IManagerApiTransport
4 | {
5 | (byte[] buf, int bytesReceived) SendReceive(ReadOnlySpan request, int initRecvBufSize = 4096);
6 | Task<(byte[] buf, int bytesReceived)> SendReceiveAsync(ReadOnlyMemory request, int initRecvBufSize = 4096, CancellationToken cancellationToken = default);
7 | }
8 |
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator/Manager/ManagerAPIJsonSerializerContext.cs:
--------------------------------------------------------------------------------
1 | using ShadowsocksUriGenerator.OnlineConfig;
2 | using System.Text.Json.Serialization;
3 |
4 | namespace ShadowsocksUriGenerator.Manager;
5 |
6 | [JsonSerializable(typeof(ManagerApiAddRequest))]
7 | [JsonSerializable(typeof(ManagerApiRemoveRequest))]
8 | [JsonSerializable(typeof(Dictionary))]
9 | [JsonSerializable(typeof(SIP008Config))]
10 | [JsonSourceGenerationOptions(
11 | DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault,
12 | IgnoreReadOnlyProperties = true,
13 | PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
14 | public partial class ManagerApiJsonSerializerContext : JsonSerializerContext
15 | {
16 | }
17 |
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator/Manager/ManagerApiRequest.cs:
--------------------------------------------------------------------------------
1 | using System.Buffers;
2 | using System.Text;
3 | using System.Text.Json;
4 | using System.Text.Json.Serialization;
5 |
6 | namespace ShadowsocksUriGenerator.Manager;
7 |
8 | public class ManagerApiAddRequest
9 | {
10 | [JsonPropertyName("server_port")]
11 | public int Port { get; set; }
12 |
13 | public string Password { get; set; } = "";
14 |
15 | public string? Method { get; set; }
16 |
17 | [JsonPropertyName("plugin")]
18 | public string? PluginName { get; set; }
19 |
20 | [JsonPropertyName("plugin_version")]
21 | public string? PluginVersion { get; set; }
22 |
23 | [JsonPropertyName("plugin_opts")]
24 | public string? PluginOptions { get; set; }
25 |
26 | [JsonPropertyName("plugin_args")]
27 | public string? PluginArguments { get; set; }
28 |
29 | public byte[] ToBytes()
30 | {
31 | var jsonBytes = JsonSerializer.SerializeToUtf8Bytes(this, ManagerApiJsonSerializerContext.Default.ManagerApiAddRequest);
32 | var requestBytes = ArrayPool.Shared.Rent(5 + jsonBytes.Length);
33 | _ = Encoding.UTF8.GetBytes("add: ", requestBytes);
34 | jsonBytes.CopyTo(requestBytes, jsonBytes.Length);
35 | return requestBytes;
36 | }
37 | }
38 |
39 | public class ManagerApiRemoveRequest
40 | {
41 | [JsonPropertyName("server_port")]
42 | public int Port { get; set; }
43 |
44 | public byte[] ToBytes()
45 | {
46 | var jsonBytes = JsonSerializer.SerializeToUtf8Bytes(this, ManagerApiJsonSerializerContext.Default.ManagerApiRemoveRequest);
47 | var requestBytes = ArrayPool.Shared.Rent(8 + jsonBytes.Length);
48 | _ = Encoding.UTF8.GetBytes("remove: ", requestBytes);
49 | jsonBytes.CopyTo(requestBytes, jsonBytes.Length);
50 | return requestBytes;
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator/Manager/ManagerApiResponse.cs:
--------------------------------------------------------------------------------
1 | namespace ShadowsocksUriGenerator.Manager;
2 |
3 | public record ManagerApiResponse(string Content)
4 | {
5 | public bool IsOk => Content.Equals("ok", StringComparison.OrdinalIgnoreCase);
6 | }
7 |
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator/Manager/ManagerApiTransport.cs:
--------------------------------------------------------------------------------
1 | using System.Buffers;
2 | using System.Net;
3 | using System.Net.Sockets;
4 |
5 | namespace ShadowsocksUriGenerator.Manager;
6 |
7 | internal class ManagerApiTransport : IManagerApiTransport, IDisposable
8 | {
9 | private readonly Lock _locker = new();
10 | private readonly SemaphoreSlim _semaphoreSlim = new(1);
11 | private readonly Socket _socket;
12 | private bool disposedValue;
13 |
14 | public ManagerApiTransport(UnixDomainSocketEndPoint endPoint)
15 | {
16 | _socket = new(AddressFamily.Unix, SocketType.Dgram, 0);
17 | _socket.Bind(endPoint);
18 | _socket.ReceiveTimeout = 1000;
19 | }
20 |
21 | public ManagerApiTransport(IPEndPoint endPoint, int receiveTimeoutMs = 5000)
22 | {
23 | _socket = new(endPoint.AddressFamily, SocketType.Dgram, ProtocolType.Udp);
24 | _socket.Connect(endPoint);
25 | _socket.ReceiveTimeout = receiveTimeoutMs;
26 | }
27 |
28 | public (byte[] buf, int bytesReceived) SendReceive(ReadOnlySpan request, int initRecvBufSize = 4096)
29 | {
30 | lock (_locker)
31 | {
32 | _ = _socket.Send(request);
33 |
34 | var buf = ArrayPool.Shared.Rent(initRecvBufSize);
35 | var received = 0;
36 |
37 | while (true)
38 | {
39 | int n;
40 | try
41 | {
42 | n = _socket.Receive(buf, received, buf.Length - received, SocketFlags.None);
43 | }
44 | catch
45 | {
46 | ArrayPool.Shared.Return(buf);
47 | throw;
48 | }
49 | received += n;
50 |
51 | if (received < buf.Length)
52 | break;
53 |
54 | var newBuf = ArrayPool.Shared.Rent(buf.Length * 2);
55 | Buffer.BlockCopy(buf, 0, newBuf, 0, buf.Length);
56 | ArrayPool.Shared.Return(buf);
57 | buf = newBuf;
58 | }
59 |
60 | return (buf, received);
61 | }
62 | }
63 |
64 | public async Task<(byte[] buf, int bytesReceived)> SendReceiveAsync(ReadOnlyMemory request, int initRecvBufSize = 4096, CancellationToken cancellationToken = default)
65 | {
66 | await _semaphoreSlim.WaitAsync(cancellationToken);
67 | try
68 | {
69 | _ = await _socket.SendAsync(request, SocketFlags.None, cancellationToken);
70 |
71 | var buf = ArrayPool.Shared.Rent(initRecvBufSize);
72 | var received = 0;
73 | var n = 0;
74 |
75 | while (true)
76 | {
77 | var receiveTask = _socket.ReceiveAsync(buf, SocketFlags.None, cancellationToken).AsTask();
78 | Task[] tasks = [receiveTask, Task.Delay(_socket.ReceiveTimeout, cancellationToken),];
79 | var finishedTask = await Task.WhenAny(tasks);
80 | if (finishedTask != receiveTask) // timeout
81 | {
82 | Dispose();
83 | }
84 |
85 | try
86 | {
87 | n = await receiveTask; // throws if disposed
88 | }
89 | catch
90 | {
91 | ArrayPool.Shared.Return(buf);
92 | throw;
93 | }
94 | received += n;
95 |
96 | if (received < buf.Length)
97 | break;
98 |
99 | var newBuf = ArrayPool.Shared.Rent(buf.Length * 2);
100 | Buffer.BlockCopy(buf, 0, newBuf, 0, buf.Length);
101 | ArrayPool.Shared.Return(buf);
102 | buf = newBuf;
103 | }
104 |
105 | return (buf, received);
106 | }
107 | finally
108 | {
109 | _semaphoreSlim.Release();
110 | }
111 | }
112 |
113 | protected virtual void Dispose(bool disposing)
114 | {
115 | if (!disposedValue)
116 | {
117 | if (disposing)
118 | {
119 | _socket.Dispose();
120 | }
121 |
122 | disposedValue = true;
123 | }
124 | }
125 |
126 | public void Dispose()
127 | {
128 | Dispose(disposing: true);
129 | GC.SuppressFinalize(this);
130 | }
131 | }
132 |
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator/OnlineConfig/DateTimeOffsetUnixTimeSecondsConverter.cs:
--------------------------------------------------------------------------------
1 | using System.Text.Json;
2 | using System.Text.Json.Serialization;
3 |
4 | namespace ShadowsocksUriGenerator.OnlineConfig;
5 |
6 | public class DateTimeOffsetUnixTimeSecondsConverter : JsonConverter
7 | {
8 | public override DateTimeOffset Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
9 | => DateTimeOffset.FromUnixTimeSeconds(reader.GetInt64());
10 |
11 | public override void Write(Utf8JsonWriter writer, DateTimeOffset value, JsonSerializerOptions options)
12 | => writer.WriteNumberValue(value.ToUnixTimeSeconds());
13 | }
14 |
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator/OnlineConfig/OOCv1ApiToken.cs:
--------------------------------------------------------------------------------
1 | namespace ShadowsocksUriGenerator.OnlineConfig;
2 |
3 | ///
4 | /// OOCv1 API access information.
5 | /// Serialize and deserialize in camelCase.
6 | ///
7 | public record OOCv1ApiToken(int Version, string BaseUrl, string Secret, string UserId, string? CertSha256);
8 |
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator/OnlineConfig/OOCv1ConfigBase.cs:
--------------------------------------------------------------------------------
1 | using System.Text.Json.Serialization;
2 |
3 | namespace ShadowsocksUriGenerator.OnlineConfig;
4 |
5 | ///
6 | /// OOCv1 config base.
7 | /// Inherit from this class and add protocol-specific properties.
8 | /// Serialize and deserialize in camelCase.
9 | ///
10 | public class OOCv1ConfigBase
11 | {
12 | ///
13 | /// Gets or sets the username.
14 | /// Optional.
15 | ///
16 | public string? Username { get; set; }
17 |
18 | ///
19 | /// Gets or sets the amount of data used in bytes.
20 | /// Optional.
21 | ///
22 | public ulong? BytesUsed { get; set; }
23 |
24 | ///
25 | /// Gets or sets the amount of data remaining in bytes.
26 | /// Optional.
27 | ///
28 | public ulong? BytesRemaining { get; set; }
29 |
30 | ///
31 | /// Gets or sets the expiry date of the configuration.
32 | /// Optional.
33 | ///
34 | [JsonConverter(typeof(DateTimeOffsetUnixTimeSecondsConverter))]
35 | public DateTimeOffset? ExpiryDate { get; set; }
36 |
37 | ///
38 | /// Gets or sets the protocols used in the configuration.
39 | ///
40 | public List Protocols { get; set; } = [];
41 | }
42 |
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator/OnlineConfig/OOCv1ShadowsocksConfig.cs:
--------------------------------------------------------------------------------
1 | namespace ShadowsocksUriGenerator.OnlineConfig;
2 |
3 | public class OOCv1ShadowsocksConfig : OOCv1ConfigBase
4 | {
5 | ///
6 | /// Gets or sets the list of Shadowsocks servers.
7 | ///
8 | public IEnumerable Shadowsocks { get; set; } = [];
9 |
10 | ///
11 | /// Initializes an OOCv1 Shadowsocks config.
12 | ///
13 | public OOCv1ShadowsocksConfig() => Protocols.Add("shadowsocks");
14 | }
15 |
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator/OnlineConfig/OOCv1ShadowsocksServer.cs:
--------------------------------------------------------------------------------
1 | using ShadowsocksUriGenerator.Protocols.Shadowsocks;
2 | using System.Text.Json.Serialization;
3 |
4 | namespace ShadowsocksUriGenerator.OnlineConfig;
5 |
6 | public class OOCv1ShadowsocksServer
7 | {
8 | ///
9 | public string Id { get; set; } = Guid.NewGuid().ToString();
10 |
11 | ///
12 | public string Name { get; set; } = "";
13 |
14 | ///
15 | [JsonPropertyName("address")]
16 | public string Host { get; set; } = "";
17 |
18 | ///
19 | public int Port { get; set; }
20 |
21 | ///
22 | public string Method { get; set; } = "2022-blake3-aes-256-gcm";
23 |
24 | ///
25 | /// Gets or sets the password for the server.
26 | ///
27 | public string Password { get; set; } = "";
28 |
29 | ///
30 | public string? PluginName { get; set; }
31 |
32 | ///
33 | public string? PluginVersion { get; set; }
34 |
35 | ///
36 | public string? PluginOptions { get; set; }
37 |
38 | ///
39 | public string? PluginArguments { get; set; }
40 |
41 | ///
42 | public string? Group { get; set; }
43 |
44 | ///
45 | public string? Owner { get; set; }
46 |
47 | ///
48 | public IEnumerable? Tags { get; set; }
49 |
50 | public OOCv1ShadowsocksServer()
51 | {
52 | }
53 |
54 | public OOCv1ShadowsocksServer(ShadowsocksServerConfig server)
55 | {
56 | Id = server.Id;
57 | Name = server.Name;
58 | Host = server.Host;
59 | Port = server.Port;
60 | Method = server.Method;
61 | Password = server.GetPassword();
62 | PluginName = server.PluginName;
63 | PluginVersion = server.PluginVersion;
64 | PluginOptions = server.PluginOptions;
65 | PluginArguments = server.PluginArguments;
66 | Group = server.Group;
67 | Owner = server.Owner;
68 | Tags = server.Tags.Any() ? server.Tags : null;
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator/OnlineConfig/OnlineConfigJsonSerializerContext.cs:
--------------------------------------------------------------------------------
1 | using System.Text.Json.Serialization;
2 |
3 | namespace ShadowsocksUriGenerator.OnlineConfig;
4 |
5 | [JsonSerializable(typeof(OOCv1ApiToken))]
6 | [JsonSerializable(typeof(OOCv1ShadowsocksConfig))]
7 | [JsonSerializable(typeof(ShadowsocksGoConfig))]
8 | [JsonSourceGenerationOptions(
9 | DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault,
10 | IgnoreReadOnlyProperties = true,
11 | PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
12 | public partial class OnlineConfigCamelCaseJsonSerializerContext : JsonSerializerContext
13 | {
14 | }
15 |
16 | [JsonSerializable(typeof(SingBoxConfig))]
17 | [JsonSerializable(typeof(SIP008Config))]
18 | [JsonSourceGenerationOptions(
19 | DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault,
20 | IgnoreReadOnlyProperties = true,
21 | PropertyNamingPolicy = JsonKnownNamingPolicy.SnakeCaseLower)]
22 | public partial class OnlineConfigSnakeCaseJsonSerializerContext : JsonSerializerContext
23 | {
24 | }
25 |
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator/OnlineConfig/SIP008Config.cs:
--------------------------------------------------------------------------------
1 | namespace ShadowsocksUriGenerator.OnlineConfig;
2 |
3 | public class SIP008Config
4 | {
5 | ///
6 | /// Gets or sets the SIP008 document version.
7 | ///
8 | public int Version { get; set; } = 1;
9 |
10 | ///
11 | /// Gets or sets the username.
12 | ///
13 | public string? Username { get; set; }
14 |
15 | ///
16 | /// Gets or sets the user ID.
17 | ///
18 | public string? Id { get; set; }
19 |
20 | ///
21 | /// Gets or sets the data usage in bytes.
22 | ///
23 | public ulong? BytesUsed { get; set; }
24 |
25 | ///
26 | /// Gets or sets the data remaining to be used in bytes.
27 | ///
28 | public ulong? BytesRemaining { get; set; }
29 |
30 | ///
31 | /// Gets or sets the list of servers.
32 | ///
33 | public IEnumerable Servers { get; set; } = [];
34 | }
35 |
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator/OnlineConfig/SIP008Server.cs:
--------------------------------------------------------------------------------
1 | using ShadowsocksUriGenerator.Protocols.Shadowsocks;
2 | using System.Text.Json.Serialization;
3 |
4 | namespace ShadowsocksUriGenerator.OnlineConfig;
5 |
6 | public class SIP008Server
7 | {
8 | ///
9 | public string Id { get; set; } = Guid.NewGuid().ToString();
10 |
11 | ///
12 | [JsonPropertyName("remarks")]
13 | public string Name { get; set; } = "";
14 |
15 | ///
16 | [JsonPropertyName("server")]
17 | public string Host { get; set; } = "";
18 |
19 | ///
20 | [JsonPropertyName("server_port")]
21 | public int Port { get; set; }
22 |
23 | ///
24 | public string Method { get; set; } = "2022-blake3-aes-256-gcm";
25 |
26 | ///
27 | public string Password { get; set; } = "";
28 |
29 | ///
30 | [JsonPropertyName("plugin")]
31 | public string? PluginName { get; set; }
32 |
33 | ///
34 | public string? PluginVersion { get; set; }
35 |
36 | ///
37 | [JsonPropertyName("plugin_opts")]
38 | public string? PluginOptions { get; set; }
39 |
40 | ///
41 | [JsonPropertyName("plugin_args")]
42 | public string? PluginArguments { get; set; }
43 |
44 | ///
45 | public string? Group { get; set; }
46 |
47 | ///
48 | public string? Owner { get; set; }
49 |
50 | ///
51 | public IEnumerable? Tags { get; set; }
52 |
53 | public SIP008Server()
54 | {
55 | }
56 |
57 | public SIP008Server(ShadowsocksServerConfig server)
58 | {
59 | Id = server.Id;
60 | Name = server.Name;
61 | Host = server.Host;
62 | Port = server.Port;
63 | Method = server.Method;
64 | Password = server.GetPassword();
65 | PluginName = server.PluginName;
66 | PluginVersion = server.PluginVersion;
67 | PluginOptions = server.PluginOptions;
68 | PluginArguments = server.PluginArguments;
69 | Group = server.Group;
70 | Owner = server.Owner;
71 | Tags = server.Tags.Any() ? server.Tags : null;
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator/OnlineConfig/ShadowsocksGoClientConfig.cs:
--------------------------------------------------------------------------------
1 | using ShadowsocksUriGenerator.Protocols.Shadowsocks;
2 | using System.Text.Json.Serialization;
3 |
4 | namespace ShadowsocksUriGenerator.OnlineConfig;
5 |
6 | public class ShadowsocksGoClientConfig
7 | {
8 | public string Name { get; set; } = "";
9 | public string? Endpoint { get; set; }
10 | public string Protocol { get; set; } = "";
11 | public int DialerFwmark { get; set; }
12 | public int DialerTrafficClass { get; set; }
13 |
14 | #region TCP
15 | public bool EnableTCP { get; set; } = true;
16 | public bool DialerTFO { get; set; } = true;
17 | #endregion
18 |
19 | #region UDP
20 | public bool EnableUDP { get; set; } = true;
21 | public int MTU { get; set; } = 1500;
22 | #endregion
23 |
24 | #region Shadowsocks
25 | public string? PSK { get; set; }
26 | [JsonPropertyName("iPSKs")]
27 | public IEnumerable? IdentityPSKs { get; set; }
28 | public string? PaddingPolicy { get; set; }
29 | #endregion
30 |
31 | ///
32 | /// Creates an empty client config.
33 | ///
34 | public ShadowsocksGoClientConfig()
35 | {
36 | }
37 |
38 | ///
39 | /// Creates a direct client config.
40 | ///
41 | /// Whether to disable TCP.
42 | /// Whether to disable TCP Fast Open.
43 | /// Whether to disable UDP.
44 | /// Set a fwmark for sockets.
45 | /// Set a traffic class for sockets.
46 | /// The path MTU between client and server.
47 | public ShadowsocksGoClientConfig(
48 | bool disableTCP,
49 | bool disableTFO,
50 | bool disableUDP,
51 | int dialerFwmark,
52 | int dialerTrafficClass,
53 | int mtu)
54 | {
55 | Name = "direct";
56 | Protocol = "direct";
57 | DialerFwmark = dialerFwmark;
58 | DialerTrafficClass = dialerTrafficClass;
59 | EnableTCP = !disableTCP;
60 | DialerTFO = !disableTFO;
61 | EnableUDP = !disableUDP;
62 | MTU = mtu;
63 | }
64 |
65 | ///
66 | /// Creates a Shadowsocks client config.
67 | ///
68 | /// The Shadowsocks server config.
69 | /// The padding policy to use for outgoing Shadowsocks traffic.
70 | /// Whether to disable TCP.
71 | /// Whether to disable TCP Fast Open.
72 | /// Whether to disable UDP.
73 | /// Set a fwmark for sockets.
74 | /// Set a traffic class for sockets.
75 | /// The path MTU between client and server.
76 | public ShadowsocksGoClientConfig(
77 | ShadowsocksServerConfig server,
78 | string? paddingPolicy,
79 | bool disableTCP,
80 | bool disableTFO,
81 | bool disableUDP,
82 | int dialerFwmark,
83 | int dialerTrafficClass,
84 | int mtu)
85 | {
86 | Name = server.Name;
87 | Endpoint = server.GetHostPort();
88 | Protocol = server.Method;
89 | DialerFwmark = dialerFwmark;
90 | DialerTrafficClass = dialerTrafficClass;
91 | EnableTCP = !disableTCP;
92 | DialerTFO = !disableTFO;
93 | EnableUDP = !disableUDP;
94 | MTU = mtu;
95 | PSK = server.UserPSK;
96 | IdentityPSKs = server.IdentityPSKs.Any() ? server.IdentityPSKs : null;
97 | PaddingPolicy = paddingPolicy;
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator/OnlineConfig/ShadowsocksGoConfig.cs:
--------------------------------------------------------------------------------
1 | namespace ShadowsocksUriGenerator.OnlineConfig;
2 |
3 | public class ShadowsocksGoConfig
4 | {
5 | public IEnumerable? Clients { get; set; }
6 | }
7 |
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator/OnlineConfig/SingBoxConfig.cs:
--------------------------------------------------------------------------------
1 | namespace ShadowsocksUriGenerator.OnlineConfig;
2 |
3 | public class SingBoxConfig
4 | {
5 | public IEnumerable? Outbounds { get; set; }
6 | }
7 |
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator/OnlineConfig/SingBoxMultiplexConfig.cs:
--------------------------------------------------------------------------------
1 | namespace ShadowsocksUriGenerator.OnlineConfig;
2 |
3 | public class SingBoxMultiplexConfig
4 | {
5 | public bool Enabled { get; set; }
6 | public string? Protocol { get; set; }
7 | public int MaxConnections { get; set; }
8 | public int MinStreams { get; set; }
9 | public int MaxStreams { get; set; }
10 | }
11 |
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator/OnlineConfig/SingBoxOutboundConfig.cs:
--------------------------------------------------------------------------------
1 | using ShadowsocksUriGenerator.Protocols.Shadowsocks;
2 |
3 | namespace ShadowsocksUriGenerator.OnlineConfig;
4 |
5 | public class SingBoxOutboundConfig
6 | {
7 | public string Type { get; set; } = "";
8 | public string Tag { get; set; } = "";
9 |
10 | #region ServerOptions
11 | public string? Server { get; set; }
12 | public int ServerPort { get; set; }
13 | #endregion
14 |
15 | #region ShadowsocksOutboundOptions
16 | public string? Method { get; set; }
17 | public string? Password { get; set; }
18 | public string? Plugin { get; set; }
19 | public string? PluginOpts { get; set; }
20 | public string? Network { get; set; }
21 | public bool UdpOverTcp { get; set; }
22 | public SingBoxMultiplexConfig? Multiplex { get; set; }
23 | #endregion
24 |
25 | #region DialerOptions
26 | public string? Detour { get; set; }
27 | public string? BindInterface { get; set; }
28 | public string? Inet4BindAddress { get; set; }
29 | public string? Inet6BindAddress { get; set; }
30 | public int RoutingMark { get; set; }
31 | public bool ReuseAddr { get; set; }
32 | public string? ConnectTimeout { get; set; }
33 | public bool TcpFastOpen { get; set; }
34 | public bool UdpFragment { get; set; }
35 | public string? DomainStrategy { get; set; }
36 | public string? FallbackDelay { get; set; }
37 | #endregion
38 |
39 | #region SelectorOutboundOptions
40 | public IEnumerable? Outbounds { get; set; }
41 | public string? Default { get; set; }
42 | #endregion
43 |
44 | public SingBoxOutboundConfig()
45 | {
46 | }
47 |
48 | public SingBoxOutboundConfig(
49 | ShadowsocksServerConfig server,
50 | string? network,
51 | bool uot,
52 | bool multiplex,
53 | string? multiplexProtocol,
54 | int multiplexMaxConnections,
55 | int multiplexMinStreams,
56 | int multiplexMaxStreams,
57 | string? detour,
58 | string? bindInterface,
59 | string? inet4BindAddress,
60 | string? inet6BindAddress,
61 | int routingMark,
62 | bool reuseAddr,
63 | string? connectTimeout,
64 | bool tcpFastOpen,
65 | bool udpFragment,
66 | string? domainStrategy,
67 | string? fallbackDelay)
68 | {
69 | Type = "shadowsocks";
70 | Tag = server.Name;
71 |
72 | Server = server.Host;
73 | ServerPort = server.Port;
74 | Method = server.Method;
75 | Password = server.GetPassword();
76 | Plugin = server.PluginName;
77 | PluginOpts = server.PluginOptions;
78 | Network = network;
79 | UdpOverTcp = uot;
80 | if (multiplex)
81 | Multiplex = new()
82 | {
83 | Enabled = true,
84 | Protocol = multiplexProtocol,
85 | MaxConnections = multiplexMaxConnections,
86 | MinStreams = multiplexMinStreams,
87 | MaxStreams = multiplexMaxStreams,
88 | };
89 |
90 | Detour = detour;
91 | BindInterface = bindInterface;
92 | Inet4BindAddress = inet4BindAddress;
93 | Inet6BindAddress = inet6BindAddress;
94 | RoutingMark = routingMark;
95 | ReuseAddr = reuseAddr;
96 | ConnectTimeout = connectTimeout;
97 | TcpFastOpen = tcpFastOpen;
98 | UdpFragment = udpFragment;
99 | DomainStrategy = domainStrategy;
100 | FallbackDelay = fallbackDelay;
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator/Outline/DateTimeOffsetUnixTimeMillisecondsConverter.cs:
--------------------------------------------------------------------------------
1 | using System.Text.Json;
2 | using System.Text.Json.Serialization;
3 |
4 | namespace ShadowsocksUriGenerator.Outline
5 | {
6 | public class DateTimeOffsetUnixTimeMillisecondsConverter : JsonConverter
7 | {
8 | public override DateTimeOffset Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
9 | => DateTimeOffset.FromUnixTimeMilliseconds(reader.GetInt64());
10 |
11 | public override void Write(Utf8JsonWriter writer, DateTimeOffset value, JsonSerializerOptions options)
12 | => writer.WriteNumberValue(value.ToUnixTimeMilliseconds());
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator/Outline/OutlineAccessKey.cs:
--------------------------------------------------------------------------------
1 | namespace ShadowsocksUriGenerator.Outline
2 | {
3 | ///
4 | /// The mutable record type that stores an Outline access key.
5 | /// It's mutable so it can be atomically updated.
6 | ///
7 | public record OutlineAccessKey
8 | {
9 | public string Id { get; set; } = "";
10 | public string Name { get; set; } = "";
11 | public string Password { get; set; } = "";
12 | public int Port { get; set; }
13 | public string Method { get; set; } = "";
14 | public OutlineDataLimit? DataLimit { get; set; }
15 | public string AccessUrl { get; set; } = "";
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator/Outline/OutlineApiClient.cs:
--------------------------------------------------------------------------------
1 | using System.Net.Http.Json;
2 | using System.Net.Security;
3 | using System.Security.Cryptography;
4 |
5 | namespace ShadowsocksUriGenerator.Outline;
6 |
7 | public sealed class OutlineApiClient : IDisposable
8 | {
9 | private readonly HttpClient _httpClient;
10 | private readonly Uri _serverInfoUri;
11 | private readonly Uri _serverNameUri;
12 | private readonly Uri _serverHostnameUri;
13 | private readonly Uri _serverMetricsUri;
14 | private readonly Uri _accessKeysUri;
15 | private readonly Uri _accessKeysPortUri;
16 | private readonly Uri _accessKeysSlashUri;
17 | private readonly Uri _dataUsageUri;
18 | private readonly Uri _dataLimitUri;
19 | private readonly bool _disposeHttpClient;
20 |
21 | ///
22 | /// Creates an Outline API client for the specified API key.
23 | ///
24 | ///
25 | /// Generic HTTP client to use when the API key does not pin a certificate fingerprint.
26 | /// This instance is ignored when the API key pins a certificate fingerprint with .
27 | /// Remember to set a timeout on the instance before passing it to this method. A 30s timeout is recommended.
28 | ///
29 | /// Outline API key.
30 | public OutlineApiClient(HttpClient httpClient, OutlineApiKey apiKey)
31 | {
32 | if (string.IsNullOrEmpty(apiKey.CertSha256))
33 | {
34 | _httpClient = httpClient;
35 | }
36 | else
37 | {
38 | SslClientAuthenticationOptions sslClientAuthenticationOptions = new()
39 | {
40 | RemoteCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) =>
41 | {
42 | string? sha256Fingerprint = certificate?.GetCertHashString(HashAlgorithmName.SHA256);
43 | return string.Equals(apiKey.CertSha256, sha256Fingerprint, StringComparison.OrdinalIgnoreCase);
44 | },
45 | };
46 | SocketsHttpHandler socketsHttpHandler = new()
47 | {
48 | SslOptions = sslClientAuthenticationOptions,
49 | };
50 | _httpClient = new(socketsHttpHandler);
51 | _disposeHttpClient = true;
52 | }
53 |
54 | Uri baseUri = apiKey.ApiUrl.AbsolutePath.EndsWith('/') ? apiKey.ApiUrl : new(apiKey.ApiUrl.AbsoluteUri + "/");
55 | _serverInfoUri = new(baseUri, "server");
56 | _serverNameUri = new(baseUri, "name");
57 | _serverHostnameUri = new(baseUri, "server/hostname-for-access-keys");
58 | _serverMetricsUri = new(baseUri, "metrics/enabled");
59 | _accessKeysUri = new(baseUri, "access-keys");
60 | _accessKeysPortUri = new(baseUri, "server/port-for-new-access-keys");
61 | _accessKeysSlashUri = new(baseUri, "access-keys/");
62 | _dataUsageUri = new(baseUri, "metrics/transfer");
63 | _dataLimitUri = new(baseUri, "server/access-key-data-limit");
64 | }
65 |
66 | ///
67 | public TimeSpan Timeout
68 | {
69 | get => _httpClient.Timeout;
70 | set => _httpClient.Timeout = value;
71 | }
72 |
73 | public void Dispose()
74 | {
75 | if (_disposeHttpClient)
76 | {
77 | _httpClient.Dispose();
78 | }
79 | }
80 |
81 | public Task GetServerInfoAsync(CancellationToken cancellationToken = default)
82 | => _httpClient.GetFromJsonAsync(_serverInfoUri, OutlineJsonSerializerContext.Default.OutlineServerInfo, cancellationToken);
83 |
84 | public Task SetServerNameAsync(string name, CancellationToken cancellationToken = default)
85 | => _httpClient.PutAsJsonAsync(_serverNameUri, new OutlineServerName(name), OutlineJsonSerializerContext.Default.OutlineServerName, cancellationToken);
86 |
87 | public Task SetServerHostnameAsync(string hostname, CancellationToken cancellationToken = default)
88 | => _httpClient.PutAsJsonAsync(_serverHostnameUri, new OutlineServerHostname(hostname), OutlineJsonSerializerContext.Default.OutlineServerHostname, cancellationToken);
89 |
90 | public Task SetServerMetricsAsync(bool enabled, CancellationToken cancellationToken = default)
91 | => _httpClient.PutAsJsonAsync(_serverMetricsUri, new OutlineMetrics(enabled), OutlineJsonSerializerContext.Default.OutlineMetrics, cancellationToken);
92 |
93 | public Task GetAccessKeysAsync(CancellationToken cancellationToken = default)
94 | => _httpClient.GetFromJsonAsync(_accessKeysUri, OutlineJsonSerializerContext.Default.OutlineAccessKeysResponse, cancellationToken);
95 |
96 | public Task CreateAccessKeyAsync(CancellationToken cancellationToken = default)
97 | => _httpClient.PostAsync(_accessKeysUri, new StringContent(string.Empty), cancellationToken);
98 |
99 | public Task SetAccessKeysPortAsync(int port, CancellationToken cancellationToken = default)
100 | => _httpClient.PutAsJsonAsync(_accessKeysPortUri, new OutlineAccessKeysPort(port), OutlineJsonSerializerContext.Default.OutlineAccessKeysPort, cancellationToken);
101 |
102 | public Task DeleteAccessKeyAsync(string id, CancellationToken cancellationToken = default)
103 | => _httpClient.DeleteAsync(new Uri(_accessKeysSlashUri, id), cancellationToken);
104 |
105 | public Task SetAccessKeyNameAsync(string id, string name, CancellationToken cancellationToken = default)
106 | => _httpClient.PutAsJsonAsync(new Uri(_accessKeysSlashUri, $"{id}/name"), new OutlineServerName(name), OutlineJsonSerializerContext.Default.OutlineServerName, cancellationToken);
107 |
108 | public Task SetAccessKeyDataLimitAsync(string id, ulong dataLimit, CancellationToken cancellationToken = default)
109 | => _httpClient.PutAsJsonAsync(new Uri(_accessKeysSlashUri, $"{id}/data-limit"), new OutlineDataLimitRequest(new(dataLimit)), OutlineJsonSerializerContext.Default.OutlineDataLimitRequest, cancellationToken);
110 |
111 | public Task DeleteAccessKeyDataLimitAsync(string id, CancellationToken cancellationToken = default)
112 | => _httpClient.DeleteAsync(new Uri(_accessKeysSlashUri, $"{id}/data-limit"), cancellationToken);
113 |
114 | public Task GetDataUsageAsync(CancellationToken cancellationToken = default)
115 | => _httpClient.GetFromJsonAsync(_dataUsageUri, OutlineJsonSerializerContext.Default.OutlineDataUsage, cancellationToken);
116 |
117 | public Task SetDataLimitAsync(ulong dataLimit, CancellationToken cancellationToken = default)
118 | => _httpClient.PutAsJsonAsync(_dataLimitUri, new OutlineDataLimitRequest(new(dataLimit)), OutlineJsonSerializerContext.Default.OutlineDataLimitRequest, cancellationToken);
119 |
120 | public Task DeleteDataLimitAsync(CancellationToken cancellationToken = default)
121 | => _httpClient.DeleteAsync(_dataLimitUri, cancellationToken);
122 | }
123 |
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator/Outline/OutlineJsonSerializerContext.cs:
--------------------------------------------------------------------------------
1 | using System.Text.Json.Serialization;
2 |
3 | namespace ShadowsocksUriGenerator.Outline;
4 |
5 | [JsonSerializable(typeof(OutlineApiKey))]
6 | [JsonSerializable(typeof(OutlineServerName))]
7 | [JsonSerializable(typeof(OutlineServerHostname))]
8 | [JsonSerializable(typeof(OutlineDataLimit))]
9 | [JsonSerializable(typeof(OutlineDataLimitRequest))]
10 | [JsonSerializable(typeof(OutlineMetrics))]
11 | [JsonSerializable(typeof(OutlineAccessKeysPort))]
12 | [JsonSerializable(typeof(OutlineAccessKeysResponse))]
13 | [JsonSerializable(typeof(OutlineDataUsage))]
14 | [JsonSerializable(typeof(OutlineAccessKey))]
15 | [JsonSerializable(typeof(OutlineServerInfo))]
16 | [JsonSourceGenerationOptions(
17 | DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault,
18 | NumberHandling = JsonNumberHandling.AllowReadingFromString,
19 | PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
20 | public partial class OutlineJsonSerializerContext : JsonSerializerContext
21 | {
22 | }
23 |
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator/Outline/OutlineModels.cs:
--------------------------------------------------------------------------------
1 | namespace ShadowsocksUriGenerator.Outline;
2 |
3 | public record OutlineApiKey(Uri ApiUrl, string? CertSha256);
4 | public record OutlineServerName(string Name);
5 | public record OutlineServerHostname(string Hostname);
6 | public record OutlineDataLimit(ulong Bytes);
7 | public record OutlineDataLimitRequest(OutlineDataLimit Limit);
8 | public record OutlineMetrics(bool MetricsEnabled);
9 | public record OutlineAccessKeysPort(int Port);
10 | public record OutlineAccessKeysResponse(List AccessKeys);
11 | public record OutlineDataUsage(Dictionary BytesTransferredByUserId);
12 |
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator/Outline/OutlineServerInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Text.Json.Serialization;
2 |
3 | namespace ShadowsocksUriGenerator.Outline
4 | {
5 | ///
6 | /// The mutable record type that stores information about an Outline server.
7 | /// It's mutable so it can be atomically updated.
8 | ///
9 | public record OutlineServerInfo
10 | {
11 | public string Name { get; set; } = "";
12 | public string ServerId { get; set; } = Guid.NewGuid().ToString();
13 | public bool MetricsEnabled { get; set; }
14 | [JsonConverter(typeof(DateTimeOffsetUnixTimeMillisecondsConverter))]
15 | public DateTimeOffset CreatedTimestampMs { get; set; } = DateTimeOffset.UtcNow;
16 | public string Version { get; set; } = "";
17 | public OutlineDataLimit? AccessKeyDataLimit { get; set; }
18 | public int PortForNewAccessKeys { get; set; }
19 | public string HostnameForAccessKeys { get; set; } = "";
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator/SSMv1/SSMv1ApiClient.cs:
--------------------------------------------------------------------------------
1 | using System.Net.Http.Json;
2 |
3 | namespace ShadowsocksUriGenerator.SSMv1;
4 |
5 | ///
6 | /// API client of Shadowsocks Server Management API v1 (SSMv1).
7 | ///
8 | public sealed class SSMv1ApiClient
9 | {
10 | private readonly HttpClient _httpClient;
11 | private readonly Uri _baseUri;
12 | private readonly Uri _usersUri;
13 | private readonly Uri _usersSlashUri;
14 | private readonly Uri _statsUri;
15 | private readonly Uri _statsClearUri;
16 |
17 | ///
18 | /// Constructs an API client.
19 | ///
20 | /// The HTTP client instance.
21 | /// The API endpoint.
22 | public SSMv1ApiClient(HttpClient httpClient, Uri baseUri)
23 | {
24 | _httpClient = httpClient;
25 | _baseUri = baseUri; // no trailing slash
26 |
27 | if (!baseUri.AbsolutePath.EndsWith('/'))
28 | baseUri = new(baseUri.AbsoluteUri + "/");
29 |
30 | _usersUri = new(baseUri, "users");
31 | _usersSlashUri = new(baseUri, "users/");
32 | _statsUri = new(baseUri, "stats");
33 | _statsClearUri = new(baseUri, "stats?clear=true");
34 | }
35 |
36 | ///
37 | /// Gets information about the server.
38 | ///
39 | /// A token that may be used to cancel the operation.
40 | /// The server information.
41 | public Task GetServerInfoAsync(CancellationToken cancellationToken = default) =>
42 | _httpClient.GetFromJsonAsync(_baseUri, SSMv1JsonSerializerContext.Default.SSMv1ServerInfo, cancellationToken);
43 |
44 | ///
45 | /// Gets the user list.
46 | ///
47 | /// A token that may be used to cancel the operation.
48 | /// The user list.
49 | public Task ListUsersAsync(CancellationToken cancellationToken = default) =>
50 | _httpClient.GetFromJsonAsync(_usersUri, SSMv1JsonSerializerContext.Default.SSMv1UserInfoList, cancellationToken);
51 |
52 | ///
53 | /// Adds the given user.
54 | ///
55 | /// The user to add.
56 | /// A token that may be used to cancel the operation.
57 | /// A representing the asynchronous operation.
58 | public async Task AddUserAsync(SSMv1UserInfo user, CancellationToken cancellationToken = default)
59 | {
60 | using HttpResponseMessage response = await _httpClient.PostAsJsonAsync(_usersUri, user, SSMv1JsonSerializerContext.Default.SSMv1UserInfo, cancellationToken);
61 | await ThrowIfNotSuccessAsync(response, cancellationToken);
62 | }
63 |
64 | ///
65 | /// Gets detailed information about the user.
66 | ///
67 | /// The username.
68 | /// A token that may be used to cancel the operation.
69 | /// Detailed user information.
70 | public async Task GetUserDetailsAsync(string username, CancellationToken cancellationToken = default)
71 | {
72 | Uri uri = GetUserUri(username);
73 | using HttpResponseMessage response = await _httpClient.GetAsync(uri, cancellationToken);
74 | await ThrowIfNotSuccessAsync(response, cancellationToken);
75 | return await response.Content.ReadFromJsonAsync(SSMv1JsonSerializerContext.Default.SSMv1UserDetails, cancellationToken);
76 | }
77 |
78 | ///
79 | /// Updates the user's PSK.
80 | ///
81 | /// The username.
82 | /// The new uPSK.
83 | /// A token that may be used to cancel the operation.
84 | /// A representing the asynchronous operation.
85 | public async Task UpdateUserAsync(string username, string uPSK, CancellationToken cancellationToken = default)
86 | {
87 | Uri uri = GetUserUri(username);
88 | SSMv1UserCred userCred = new()
89 | {
90 | UserPSK = uPSK,
91 | };
92 | using HttpResponseMessage response = await _httpClient.PatchAsJsonAsync(uri, userCred, SSMv1JsonSerializerContext.Default.SSMv1UserCred, cancellationToken);
93 | await ThrowIfNotSuccessAsync(response, cancellationToken);
94 | }
95 |
96 | ///
97 | /// Deletes the specified user.
98 | ///
99 | /// The username.
100 | /// A token that may be used to cancel the operation.
101 | /// A representing the asynchronous operation.
102 | public async Task DeleteUserAsync(string username, CancellationToken cancellationToken = default)
103 | {
104 | Uri uri = GetUserUri(username);
105 | using HttpResponseMessage response = await _httpClient.DeleteAsync(uri, cancellationToken);
106 | await ThrowIfNotSuccessAsync(response, cancellationToken);
107 | }
108 |
109 | private Uri GetUserUri(string username) => new(_usersSlashUri, username);
110 |
111 | private static async Task ThrowIfNotSuccessAsync(HttpResponseMessage response, CancellationToken cancellationToken)
112 | {
113 | if (!response.IsSuccessStatusCode)
114 | {
115 | SSMv1Error? error = await response.Content.ReadFromJsonAsync(SSMv1JsonSerializerContext.Default.SSMv1Error, cancellationToken);
116 | throw new SSMv1ApiException(error);
117 | }
118 | }
119 |
120 | ///
121 | /// Gets traffic stats.
122 | ///
123 | /// A token that may be used to cancel the operation.
124 | /// Server's traffic stats.
125 | public Task GetStatsAsync(CancellationToken cancellationToken = default) =>
126 | _httpClient.GetFromJsonAsync(_statsUri, SSMv1JsonSerializerContext.Default.SSMv1Stats, cancellationToken);
127 |
128 | ///
129 | /// Gets and clears traffic stats.
130 | ///
131 | /// A token that may be used to cancel the operation.
132 | /// Server's traffic stats.
133 | public Task GetAndClearStatsAsync(CancellationToken cancellationToken = default) =>
134 | _httpClient.GetFromJsonAsync(_statsClearUri, SSMv1JsonSerializerContext.Default.SSMv1Stats, cancellationToken);
135 | }
136 |
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator/SSMv1/SSMv1ApiException.cs:
--------------------------------------------------------------------------------
1 | namespace ShadowsocksUriGenerator.SSMv1;
2 |
3 | public class SSMv1ApiException : Exception
4 | {
5 | public SSMv1ApiException()
6 | { }
7 |
8 | public SSMv1ApiException(string? message) : base(message)
9 | { }
10 |
11 | public SSMv1ApiException(string? message, Exception? innerException) : base(message, innerException)
12 | { }
13 |
14 | public SSMv1ApiException(SSMv1Error? error) : base(error?.Error)
15 | { }
16 | }
17 |
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator/SSMv1/SSMv1Error.cs:
--------------------------------------------------------------------------------
1 | namespace ShadowsocksUriGenerator.SSMv1;
2 |
3 | ///
4 | /// Standard error response.
5 | ///
6 | public class SSMv1Error
7 | {
8 | ///
9 | /// Gets or sets the error message.
10 | ///
11 | public required string Error { get; set; }
12 | }
13 |
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator/SSMv1/SSMv1JsonSerializerContext.cs:
--------------------------------------------------------------------------------
1 | using System.Text.Json.Serialization;
2 |
3 | namespace ShadowsocksUriGenerator.SSMv1;
4 |
5 | [JsonSerializable(typeof(SSMv1Error))]
6 | [JsonSerializable(typeof(SSMv1ServerInfo))]
7 | [JsonSerializable(typeof(SSMv1Stats))]
8 | [JsonSerializable(typeof(SSMv1UserCred))]
9 | [JsonSerializable(typeof(SSMv1UserInfo))]
10 | [JsonSerializable(typeof(SSMv1UserInfoList))]
11 | [JsonSerializable(typeof(SSMv1UserDetails))]
12 | [JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
13 | public partial class SSMv1JsonSerializerContext : JsonSerializerContext
14 | {
15 | }
16 |
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator/SSMv1/SSMv1ServerInfo.cs:
--------------------------------------------------------------------------------
1 | namespace ShadowsocksUriGenerator.SSMv1;
2 |
3 | ///
4 | /// Server information response.
5 | ///
6 | public class SSMv1ServerInfo
7 | {
8 | ///
9 | /// Gets or sets the server name.
10 | ///
11 | public required string Server { get; set; }
12 |
13 | ///
14 | /// Gets or sets the SSM API version.
15 | ///
16 | public required string ApiVersion { get; set; }
17 | }
18 |
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator/SSMv1/SSMv1Stats.cs:
--------------------------------------------------------------------------------
1 | namespace ShadowsocksUriGenerator.SSMv1;
2 |
3 | ///
4 | /// Traffic stats.
5 | ///
6 | public class SSMv1StatsBase
7 | {
8 | ///
9 | /// Gets or sets the number of UDP packets transferred from server to client.
10 | ///
11 | public ulong DownlinkPackets { get; set; }
12 |
13 | ///
14 | /// Gets or sets the number of TCP and UDP payload bytes transferred from server to client.
15 | ///
16 | public ulong DownlinkBytes { get; set; }
17 |
18 | ///
19 | /// Gets or sets the number of UDP packets transferred from client to server.
20 | ///
21 | public ulong UplinkPackets { get; set; }
22 |
23 | ///
24 | /// Gets or sets the number of TCP and UDP payload bytes transferred from client to server.
25 | ///
26 | public ulong UplinkBytes { get; set; }
27 |
28 | ///
29 | /// Gets or sets the number of TCP sessions.
30 | ///
31 | public ulong TcpSessions { get; set; }
32 |
33 | ///
34 | /// Gets or sets the number of UDP sessions.
35 | ///
36 | public ulong UdpSessions { get; set; }
37 |
38 | ///
39 | /// Resets the stats to zero.
40 | ///
41 | public void Clear()
42 | {
43 | DownlinkPackets = 0UL;
44 | DownlinkBytes = 0UL;
45 | UplinkPackets = 0UL;
46 | UplinkBytes = 0UL;
47 | TcpSessions = 0UL;
48 | UdpSessions = 0UL;
49 | }
50 | }
51 |
52 | ///
53 | /// Traffic stats response.
54 | ///
55 | public class SSMv1Stats : SSMv1StatsBase
56 | {
57 | ///
58 | /// Per-user traffic stats.
59 | ///
60 | public SSMv1UserStats[] Users { get; set; } = [];
61 | }
62 |
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator/SSMv1/SSMv1User.cs:
--------------------------------------------------------------------------------
1 | using System.Text.Json.Serialization;
2 |
3 | namespace ShadowsocksUriGenerator.SSMv1;
4 |
5 | ///
6 | /// Contains the uPSK only.
7 | ///
8 | public class SSMv1UserCred
9 | {
10 | ///
11 | /// Gets or sets the user's PSK.
12 | ///
13 | [JsonPropertyName("uPSK")]
14 | public required string UserPSK { get; set; }
15 | }
16 |
17 | ///
18 | /// Contains a user's username and uPSK.
19 | ///
20 | public class SSMv1UserInfo
21 | {
22 | ///
23 | /// Gets or sets the username.
24 | ///
25 | public required string Username { get; set; }
26 |
27 | ///
28 | /// Gets or sets the user's PSK.
29 | ///
30 | [JsonPropertyName("uPSK")]
31 | public required string UserPSK { get; set; }
32 | }
33 |
34 | ///
35 | /// Contains a list of .
36 | ///
37 | public class SSMv1UserInfoList
38 | {
39 | ///
40 | /// Gets or sets the list of users.
41 | ///
42 | public required SSMv1UserInfo[] Users { get; set; }
43 | }
44 |
45 | ///
46 | /// Contains the username and the user's traffic stats.
47 | ///
48 | public class SSMv1UserStats : SSMv1StatsBase
49 | {
50 | ///
51 | /// Gets or sets the username.
52 | ///
53 | public required string Username { get; set; }
54 | }
55 |
56 | ///
57 | /// Contains the user's username, uPSK, and traffic stats.
58 | ///
59 | public class SSMv1UserDetails : SSMv1StatsBase
60 | {
61 | ///
62 | /// Gets or sets the username.
63 | ///
64 | public required string Username { get; set; }
65 |
66 | ///
67 | /// Gets or sets the user's PSK.
68 | ///
69 | [JsonPropertyName("uPSK")]
70 | public required string UserPSK { get; set; }
71 | }
72 |
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator/SettingsJsonSerializerContext.cs:
--------------------------------------------------------------------------------
1 | using System.Text.Json;
2 | using System.Text.Json.Serialization;
3 |
4 | namespace ShadowsocksUriGenerator;
5 |
6 | [JsonSerializable(typeof(Settings))]
7 | [JsonSourceGenerationOptions(
8 | AllowTrailingCommas = true,
9 | IgnoreReadOnlyProperties = true,
10 | ReadCommentHandling = JsonCommentHandling.Skip,
11 | WriteIndented = true)]
12 | public partial class SettingsJsonSerializerContext : JsonSerializerContext
13 | {
14 | }
15 |
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator/ShadowsocksUriGenerator.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net9.0
5 | enable
6 | enable
7 | true
8 | 5.2.0
9 | database64128
10 | Shadowsocks URI Generator Library
11 | Shadowsocks URI Generator is a management and distribution platform for censorship circumvention services.
12 | © 2023 database64128
13 | LICENSE
14 | https://github.com/database64128/shadowsocks-uri-generator
15 | ss-uri-gen.png
16 | https://github.com/database64128/shadowsocks-uri-generator
17 | Public
18 | shadowsocks;online-config;open-online-config;outline-server
19 | true
20 | false
21 | CS1591
22 |
23 |
24 |
25 |
26 | True
27 |
28 |
29 |
30 | True
31 |
32 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator/SortBy.cs:
--------------------------------------------------------------------------------
1 | namespace ShadowsocksUriGenerator
2 | {
3 | public enum SortBy
4 | {
5 | DefaultAscending = 0,
6 | DefaultDescending = 1,
7 | NameAscending = 2,
8 | NameDescending = 3,
9 | DataUsedAscending = 4,
10 | DataUsedDescending = 5,
11 | DataRemainingAscending = 6,
12 | DataRemainingDescending = 7,
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator/Utils/FileHelper.cs:
--------------------------------------------------------------------------------
1 | using System.Text.Json;
2 | using System.Text.Json.Serialization.Metadata;
3 |
4 | namespace ShadowsocksUriGenerator.Utils
5 | {
6 | public static class FileHelper
7 | {
8 | ///
9 | /// Loads data from a JSON file.
10 | ///
11 | /// Data object type.
12 | /// JSON file name.
13 | /// Metadata about the type to convert.
14 | /// A token that may be used to cancel the read operation.
15 | ///
16 | /// A ValueTuple containing a data object loaded from the JSON file and an error message.
17 | /// The error message is null if no errors occurred.
18 | ///
19 | public static async Task<(T, string? errMsg)> LoadJsonAsync(string filename, JsonTypeInfo jsonTypeInfo, CancellationToken cancellationToken = default) where T : class, new()
20 | {
21 | if (!File.Exists(filename))
22 | return (new(), null);
23 |
24 | T? jsonData = null;
25 | string? errMsg = null;
26 | FileStream? jsonFile = null;
27 |
28 | try
29 | {
30 | jsonFile = new(filename, FileMode.Open);
31 | jsonData = await JsonSerializer.DeserializeAsync(jsonFile, jsonTypeInfo, cancellationToken);
32 | }
33 | catch (Exception ex)
34 | {
35 | errMsg = $"Error: failed to load {filename}: {ex.Message}";
36 | }
37 | finally
38 | {
39 | if (jsonFile is not null)
40 | await jsonFile.DisposeAsync();
41 | }
42 |
43 | jsonData ??= new();
44 | return (jsonData, errMsg);
45 | }
46 |
47 | ///
48 | /// Saves data to a JSON file.
49 | ///
50 | /// Data object type.
51 | /// JSON file name.
52 | /// The data object to save.
53 | /// Metadata about the type to convert.
54 | /// Always overwrite the original file.
55 | /// Do not create `filename.old` as backup.
56 | /// A token that may be used to cancel the write operation.
57 | /// An error message. Null if no errors occurred.
58 | public static async Task SaveJsonAsync(
59 | string filename,
60 | T jsonData,
61 | JsonTypeInfo jsonTypeInfo,
62 | bool alwaysOverwrite = false,
63 | bool noBackup = false,
64 | CancellationToken cancellationToken = default)
65 | {
66 | string? errMsg = null;
67 | FileStream? jsonFile = null;
68 |
69 | try
70 | {
71 | // create directory
72 | var directoryPath = Path.GetDirectoryName(filename);
73 | if (!string.IsNullOrEmpty(directoryPath))
74 | Directory.CreateDirectory(directoryPath);
75 |
76 | // save JSON
77 | if (alwaysOverwrite || !File.Exists(filename)) // alwaysOverwrite or file doesn't exist. Just write to it.
78 | {
79 | jsonFile = new(filename, FileMode.Create);
80 | await JsonSerializer.SerializeAsync(jsonFile, jsonData, jsonTypeInfo, cancellationToken);
81 | }
82 | else // File exists. Write to `filename.new` and then replace with the new file and creates backup `filename.old`.
83 | {
84 | jsonFile = new($"{filename}.new", FileMode.Create);
85 | await JsonSerializer.SerializeAsync(jsonFile, jsonData, jsonTypeInfo, cancellationToken);
86 | jsonFile.Close();
87 | File.Replace($"{filename}.new", filename, noBackup ? null : $"{filename}.old");
88 | }
89 | }
90 | catch (Exception ex)
91 | {
92 | errMsg = $"Error: failed to save {filename}: {ex.Message}";
93 | }
94 | finally
95 | {
96 | if (jsonFile is not null)
97 | await jsonFile.DisposeAsync();
98 | }
99 |
100 | return errMsg;
101 | }
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator/Utils/InteractionHelper.cs:
--------------------------------------------------------------------------------
1 | namespace ShadowsocksUriGenerator.Utils;
2 |
3 | public static class InteractionHelper
4 | {
5 | ///
6 | /// Tries to parse a data limit string.
7 | ///
8 | /// The data limit string to parse.
9 | /// The parsed data limit in bytes.
10 | /// True on successful parsing. False on failure.
11 | public static bool TryParseDataLimitString(ReadOnlySpan dataLimit, out ulong dataLimitInBytes)
12 | {
13 | dataLimitInBytes = 0UL;
14 |
15 | if (dataLimit.Length == 0)
16 | return false;
17 |
18 | var multiplier = dataLimit[^1] switch
19 | {
20 | 'K' => 1024UL,
21 | 'M' => 1024UL * 1024UL,
22 | 'G' => 1024UL * 1024UL * 1024UL,
23 | 'T' => 1024UL * 1024UL * 1024UL * 1024UL,
24 | 'P' => 1024UL * 1024UL * 1024UL * 1024UL * 1024UL,
25 | 'E' => 1024UL * 1024UL * 1024UL * 1024UL * 1024UL * 1024UL,
26 | _ => 1UL,
27 | };
28 |
29 | if (multiplier == 1UL)
30 | {
31 | return ulong.TryParse(dataLimit, out dataLimitInBytes);
32 | }
33 | else if (ulong.TryParse(dataLimit[0..^1], out var dataLimitBeforeMultiplication))
34 | {
35 | dataLimitInBytes = dataLimitBeforeMultiplication * multiplier;
36 | return true;
37 | }
38 | else
39 | {
40 | return false;
41 | }
42 | }
43 |
44 | ///
45 | /// Converts a data representation in bytes
46 | /// to a human readable data string.
47 | ///
48 | ///
49 | /// The amount of data in bytes.
50 | ///
51 | ///
52 | /// Whether to use 1024-based 'GiB', 'TiB' instead of 1000-based 'GB', 'TB'.
53 | /// Defaults to false, or 1000-based 'GB', 'TB'.
54 | ///
55 | ///
56 | /// Whether the returned string has a trailing 'B' representing bytes.
57 | /// Defaults to true, or 'GB', 'TB'.
58 | /// Set to false for 'G', 'T'.
59 | ///
60 | ///
61 | /// A human readable string representation of the amount of data.
62 | ///
63 | public static string HumanReadableDataString(ulong dataInBytes, bool middle_i = false, bool trailingB = true)
64 | {
65 | if (middle_i)
66 | return HumanReadableDataString1024(dataInBytes, middle_i, trailingB);
67 | else
68 | return HumanReadableDataString1000(dataInBytes, trailingB);
69 | }
70 |
71 | ///
72 | /// Converts a data representation in bytes
73 | /// to a human readable data string
74 | /// using 1000 as the conversion rate.
75 | ///
76 | ///
77 | /// The amount of data in bytes.
78 | ///
79 | ///
80 | /// Whether the returned string has a trailing 'B' representing bytes.
81 | /// Defaults to true, or 'GB', 'TB'.
82 | /// Set to false for 'G', 'T'.
83 | ///
84 | ///
85 | /// A human readable string representation of the amount of data.
86 | ///
87 | public static string HumanReadableDataString1000(ulong dataInBytes, bool trailingB = true)
88 | {
89 | var stringTail = trailingB ? "B" : "";
90 |
91 | return dataInBytes switch
92 | {
93 | < 1000UL => $"{dataInBytes}{(trailingB ? " B" : "")}",
94 | < 1000UL * 1000UL => $"{dataInBytes / 1000.0:G4} K{stringTail}",
95 | < 1000UL * 1000UL * 1000UL => $"{dataInBytes / 1000.0 / 1000.0:G4} M{stringTail}",
96 | < 1000UL * 1000UL * 1000UL * 1000UL => $"{dataInBytes / 1000.0 / 1000.0 / 1000.0:G4} G{stringTail}",
97 | < 1000UL * 1000UL * 1000UL * 1000UL * 1000UL => $"{dataInBytes / 1000.0 / 1000.0 / 1000.0 / 1000.0:G4} T{stringTail}",
98 | < 1000UL * 1000UL * 1000UL * 1000UL * 1000UL * 1000UL => $"{dataInBytes / 1000.0 / 1000.0 / 1000.0 / 1000.0 / 1000.0:G4} P{stringTail}",
99 | _ => $"{dataInBytes / 1000.0 / 1000.0 / 1000.0 / 1000.0 / 1000.0 / 1000.0:G4} E{stringTail}",
100 | };
101 | }
102 |
103 | ///
104 | /// Converts a data representation in bytes
105 | /// to a human readable data string
106 | /// using 1024 as the conversion rate.
107 | ///
108 | ///
109 | /// The amount of data in bytes.
110 | ///
111 | ///
112 | /// Whether to return 'GiB', 'TiB' instead of 'GB', 'TB'.
113 | /// Defaults to true, or 'GiB', 'TiB'.
114 | /// This doesn't affect the conversion rate.
115 | ///
116 | ///
117 | /// Whether the returned string has a trailing 'B' representing bytes.
118 | /// Defaults to true, or 'GiB', 'TiB'.
119 | /// Set to false for 'Gi', 'Ti'.
120 | ///
121 | ///
122 | /// A human readable string representation of the amount of data.
123 | ///
124 | public static string HumanReadableDataString1024(ulong dataInBytes, bool middle_i = true, bool trailingB = true)
125 | {
126 | var stringTail = $"{(middle_i ? "i" : "")}{(trailingB ? "B" : "")}";
127 |
128 | return dataInBytes switch
129 | {
130 | < 1024UL => $"{dataInBytes}{(trailingB ? " B" : "")}",
131 | < 1024UL * 1024UL => $"{dataInBytes / 1024.0:G4} K{stringTail}",
132 | < 1024UL * 1024UL * 1024UL => $"{dataInBytes / 1024.0 / 1024.0:G4} M{stringTail}",
133 | < 1024UL * 1024UL * 1024UL * 1024UL => $"{dataInBytes / 1024.0 / 1024.0 / 1024.0:G4} G{stringTail}",
134 | < 1024UL * 1024UL * 1024UL * 1024UL * 1024UL => $"{dataInBytes / 1024.0 / 1024.0 / 1024.0 / 1024.0:G4} T{stringTail}",
135 | < 1024UL * 1024UL * 1024UL * 1024UL * 1024UL * 1024UL => $"{dataInBytes / 1024.0 / 1024.0 / 1024.0 / 1024.0 / 1024.0:G4} P{stringTail}",
136 | _ => $"{dataInBytes / 1024.0 / 1024.0 / 1024.0 / 1024.0 / 1024.0 / 1024.0:G4} E{stringTail}",
137 | };
138 | }
139 | }
140 |
--------------------------------------------------------------------------------
/ShadowsocksUriGenerator/ss-uri-gen.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/database64128/shadowsocks-uri-generator/d9bcc8eacf6293fb8ed15b72de04081584bce9af/ShadowsocksUriGenerator/ss-uri-gen.png
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 | # 📚 Shadowsocks URI Generator Documentation
2 |
3 | Docs in this directory are guides with more thorough steps.
4 |
5 | ## Contents
6 |
7 | - [Reverse Proxy Guide](reverse-proxy-guide.md)
8 |
--------------------------------------------------------------------------------
/docs/reverse-proxy-guide.md:
--------------------------------------------------------------------------------
1 | # Reverse Proxy Guide
2 |
3 | The Shadowsocks URI Generator API server is required to be used with a reverse proxy.
4 |
5 | ## Design Choices
6 |
7 | The client real IP is obtained from these sources following the order of `X-Real-IP -> X-Forwarded-For -> Connecting IP`. The API server doesn't verify the authenticity of these headers. Instead, it's the admin's responsibility to configure the reverse proxy to make sure the header used by the API server contains information from a reliable source. This choice is made for the following reasons:
8 |
9 | 1. `Microsoft.AspNetCore.HttpOverrides.IPNetwork` doesn't have a parse method out of box. We'd have to write our own parser if we decided to read `ForwardedHeadersOptions.KnownNetworks` from `appsettings.json`.
10 | 2. NGINX has [`ngx_http_realip_module`](http://nginx.org/en/docs/http/ngx_http_realip_module.html) that collects the client real IP from configured trusted sources and sets the `$remote_addr` variable.
11 | 3. Cloudflare reverse proxy sets the `CF-Connecting-IP` header and appends/sets the `X-Forwarded-For` header.
12 | 4. If the admin wants to make sure the client real IP is obtained from a trusted source, they can configure NGINX to set the `X-Real-IP` header. If the admin doesn't care whether the client IP is authentic, it just works out of box. I believe this is the perfect balance of security and usability.
13 |
14 | ## NGINX Example
15 |
16 | ```nginx
17 | http {
18 | server {
19 | location /ad304cf2-98f8-44d3-824d-b8931d5e724c/ {
20 | # The / at the end is significant.
21 | proxy_pass http://[::1]:20477/;
22 |
23 | proxy_pass_request_headers on;
24 |
25 | # Other possible values: $proxy_host, $http_host
26 | proxy_set_header Host $host;
27 |
28 | proxy_set_header X-Real-IP $remote_addr;
29 |
30 | # Use this if this is the terminating reverse proxy.
31 | # $proxy_add_x_forwarded_for is an alias for '$http_x_forwarded_for, $remote_addr'.
32 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
33 |
34 | # Use this if the incoming traffic is proxied.
35 | #proxy_set_header X-Forwarded-For '$http_x_forwarded_for, $realip_remote_addr';
36 |
37 | proxy_set_header X-Forwarded-Proto $scheme;
38 | proxy_set_header X-Forwarded-Host $http_host;
39 |
40 | proxy_set_header Upgrade $http_upgrade;
41 | proxy_set_header Connection $http_connection;
42 | }
43 | }
44 | }
45 | ```
46 |
47 | If you use Cloudflare reverse proxy, include this snippet in your `http` block:
48 |
49 | ```nginx
50 | # Trust Cloudflare IP addresses to get client IP.
51 | # Source: https://www.cloudflare.com/ips/
52 | # Last updated: 2021-07-18
53 |
54 | set_real_ip_from 103.21.244.0/22;
55 | set_real_ip_from 103.22.200.0/22;
56 | set_real_ip_from 103.31.4.0/22;
57 | set_real_ip_from 104.16.0.0/13;
58 | set_real_ip_from 104.24.0.0/14;
59 | set_real_ip_from 108.162.192.0/18;
60 | set_real_ip_from 131.0.72.0/22;
61 | set_real_ip_from 141.101.64.0/18;
62 | set_real_ip_from 162.158.0.0/15;
63 | set_real_ip_from 172.64.0.0/13;
64 | set_real_ip_from 173.245.48.0/20;
65 | set_real_ip_from 188.114.96.0/20;
66 | set_real_ip_from 190.93.240.0/20;
67 | set_real_ip_from 197.234.240.0/22;
68 | set_real_ip_from 198.41.128.0/17;
69 | set_real_ip_from 2400:cb00::/32;
70 | set_real_ip_from 2606:4700::/32;
71 | set_real_ip_from 2803:f800::/32;
72 | set_real_ip_from 2405:b500::/32;
73 | set_real_ip_from 2405:8100::/32;
74 | set_real_ip_from 2a06:98c0::/29;
75 | set_real_ip_from 2c0f:f248::/32;
76 | real_ip_header X-Forwarded-For;
77 | real_ip_recursive on;
78 | ```
79 |
--------------------------------------------------------------------------------
/nuget.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3 | "extends": [
4 | "github>database64128/renovate-config:dotnet"
5 | ],
6 | "packageRules": [
7 | {
8 | "matchPackageNames": ["System.CommandLine"],
9 | "schedule": ["monthly"]
10 | }
11 | ]
12 | }
13 |
--------------------------------------------------------------------------------
/shadowsocks-uri-generator.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 17
4 | VisualStudioVersion = 17.0.32014.148
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ShadowsocksUriGenerator", "ShadowsocksUriGenerator\ShadowsocksUriGenerator.csproj", "{DE625CF7-725A-4459-90A7-47B46D3A3F95}"
7 | EndProject
8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ShadowsocksUriGenerator.Tests", "ShadowsocksUriGenerator.Tests\ShadowsocksUriGenerator.Tests.csproj", "{5FF283B5-1F60-4CEF-A878-9E2461F3B1FB}"
9 | EndProject
10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ShadowsocksUriGenerator.Chatbot.Telegram", "ShadowsocksUriGenerator.Chatbot.Telegram\ShadowsocksUriGenerator.Chatbot.Telegram.csproj", "{CAEE0D80-233B-41EA-8482-9B504AE0FED1}"
11 | EndProject
12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ShadowsocksUriGenerator.Chatbot.Telegram.Tests", "ShadowsocksUriGenerator.Chatbot.Telegram.Tests\ShadowsocksUriGenerator.Chatbot.Telegram.Tests.csproj", "{E23E109C-BF07-4A58-8311-38DA2D485AFA}"
13 | EndProject
14 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ShadowsocksUriGenerator.CLI", "ShadowsocksUriGenerator.CLI\ShadowsocksUriGenerator.CLI.csproj", "{CFFB4519-8EE5-423E-9093-2DAD3ECE8235}"
15 | EndProject
16 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ShadowsocksUriGenerator.CLI.Utils", "ShadowsocksUriGenerator.CLI.Utils\ShadowsocksUriGenerator.CLI.Utils.csproj", "{E90E6C41-9160-4DBA-BA80-DFBCF335194C}"
17 | EndProject
18 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ShadowsocksUriGenerator.CLI.Tests", "ShadowsocksUriGenerator.CLI.Tests\ShadowsocksUriGenerator.CLI.Tests.csproj", "{D035E8E4-F380-4607-9BC8-3F610ADF9F5D}"
19 | EndProject
20 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ShadowsocksUriGenerator.Rescue.CLI", "ShadowsocksUriGenerator.Rescue.CLI\ShadowsocksUriGenerator.Rescue.CLI.csproj", "{46983015-7962-49F6-946E-7870E9C79026}"
21 | EndProject
22 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ShadowsocksUriGenerator.Rescue", "ShadowsocksUriGenerator.Rescue\ShadowsocksUriGenerator.Rescue.csproj", "{AC66E10A-566C-419A-967F-FF363C62F8C2}"
23 | EndProject
24 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ShadowsocksUriGenerator.Services", "ShadowsocksUriGenerator.Services\ShadowsocksUriGenerator.Services.csproj", "{8D8E1F9F-0AF3-416D-8B5D-16A411326FE7}"
25 | EndProject
26 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ShadowsocksUriGenerator.Server", "ShadowsocksUriGenerator.Server\ShadowsocksUriGenerator.Server.csproj", "{AC7A6CED-4EF1-4B56-B7BA-BC4F0FECFB6E}"
27 | EndProject
28 | Global
29 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
30 | Debug|Any CPU = Debug|Any CPU
31 | Release|Any CPU = Release|Any CPU
32 | EndGlobalSection
33 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
34 | {DE625CF7-725A-4459-90A7-47B46D3A3F95}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
35 | {DE625CF7-725A-4459-90A7-47B46D3A3F95}.Debug|Any CPU.Build.0 = Debug|Any CPU
36 | {DE625CF7-725A-4459-90A7-47B46D3A3F95}.Release|Any CPU.ActiveCfg = Release|Any CPU
37 | {DE625CF7-725A-4459-90A7-47B46D3A3F95}.Release|Any CPU.Build.0 = Release|Any CPU
38 | {5FF283B5-1F60-4CEF-A878-9E2461F3B1FB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
39 | {5FF283B5-1F60-4CEF-A878-9E2461F3B1FB}.Debug|Any CPU.Build.0 = Debug|Any CPU
40 | {5FF283B5-1F60-4CEF-A878-9E2461F3B1FB}.Release|Any CPU.ActiveCfg = Release|Any CPU
41 | {5FF283B5-1F60-4CEF-A878-9E2461F3B1FB}.Release|Any CPU.Build.0 = Release|Any CPU
42 | {CAEE0D80-233B-41EA-8482-9B504AE0FED1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
43 | {CAEE0D80-233B-41EA-8482-9B504AE0FED1}.Debug|Any CPU.Build.0 = Debug|Any CPU
44 | {CAEE0D80-233B-41EA-8482-9B504AE0FED1}.Release|Any CPU.ActiveCfg = Release|Any CPU
45 | {CAEE0D80-233B-41EA-8482-9B504AE0FED1}.Release|Any CPU.Build.0 = Release|Any CPU
46 | {E23E109C-BF07-4A58-8311-38DA2D485AFA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
47 | {E23E109C-BF07-4A58-8311-38DA2D485AFA}.Debug|Any CPU.Build.0 = Debug|Any CPU
48 | {E23E109C-BF07-4A58-8311-38DA2D485AFA}.Release|Any CPU.ActiveCfg = Release|Any CPU
49 | {E23E109C-BF07-4A58-8311-38DA2D485AFA}.Release|Any CPU.Build.0 = Release|Any CPU
50 | {CFFB4519-8EE5-423E-9093-2DAD3ECE8235}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
51 | {CFFB4519-8EE5-423E-9093-2DAD3ECE8235}.Debug|Any CPU.Build.0 = Debug|Any CPU
52 | {CFFB4519-8EE5-423E-9093-2DAD3ECE8235}.Release|Any CPU.ActiveCfg = Release|Any CPU
53 | {CFFB4519-8EE5-423E-9093-2DAD3ECE8235}.Release|Any CPU.Build.0 = Release|Any CPU
54 | {E90E6C41-9160-4DBA-BA80-DFBCF335194C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
55 | {E90E6C41-9160-4DBA-BA80-DFBCF335194C}.Debug|Any CPU.Build.0 = Debug|Any CPU
56 | {E90E6C41-9160-4DBA-BA80-DFBCF335194C}.Release|Any CPU.ActiveCfg = Release|Any CPU
57 | {E90E6C41-9160-4DBA-BA80-DFBCF335194C}.Release|Any CPU.Build.0 = Release|Any CPU
58 | {D035E8E4-F380-4607-9BC8-3F610ADF9F5D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
59 | {D035E8E4-F380-4607-9BC8-3F610ADF9F5D}.Debug|Any CPU.Build.0 = Debug|Any CPU
60 | {D035E8E4-F380-4607-9BC8-3F610ADF9F5D}.Release|Any CPU.ActiveCfg = Release|Any CPU
61 | {D035E8E4-F380-4607-9BC8-3F610ADF9F5D}.Release|Any CPU.Build.0 = Release|Any CPU
62 | {46983015-7962-49F6-946E-7870E9C79026}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
63 | {46983015-7962-49F6-946E-7870E9C79026}.Debug|Any CPU.Build.0 = Debug|Any CPU
64 | {46983015-7962-49F6-946E-7870E9C79026}.Release|Any CPU.ActiveCfg = Release|Any CPU
65 | {46983015-7962-49F6-946E-7870E9C79026}.Release|Any CPU.Build.0 = Release|Any CPU
66 | {AC66E10A-566C-419A-967F-FF363C62F8C2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
67 | {AC66E10A-566C-419A-967F-FF363C62F8C2}.Debug|Any CPU.Build.0 = Debug|Any CPU
68 | {AC66E10A-566C-419A-967F-FF363C62F8C2}.Release|Any CPU.ActiveCfg = Release|Any CPU
69 | {AC66E10A-566C-419A-967F-FF363C62F8C2}.Release|Any CPU.Build.0 = Release|Any CPU
70 | {8D8E1F9F-0AF3-416D-8B5D-16A411326FE7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
71 | {8D8E1F9F-0AF3-416D-8B5D-16A411326FE7}.Debug|Any CPU.Build.0 = Debug|Any CPU
72 | {8D8E1F9F-0AF3-416D-8B5D-16A411326FE7}.Release|Any CPU.ActiveCfg = Release|Any CPU
73 | {8D8E1F9F-0AF3-416D-8B5D-16A411326FE7}.Release|Any CPU.Build.0 = Release|Any CPU
74 | {AC7A6CED-4EF1-4B56-B7BA-BC4F0FECFB6E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
75 | {AC7A6CED-4EF1-4B56-B7BA-BC4F0FECFB6E}.Debug|Any CPU.Build.0 = Debug|Any CPU
76 | {AC7A6CED-4EF1-4B56-B7BA-BC4F0FECFB6E}.Release|Any CPU.ActiveCfg = Release|Any CPU
77 | {AC7A6CED-4EF1-4B56-B7BA-BC4F0FECFB6E}.Release|Any CPU.Build.0 = Release|Any CPU
78 | EndGlobalSection
79 | GlobalSection(SolutionProperties) = preSolution
80 | HideSolutionNode = FALSE
81 | EndGlobalSection
82 | GlobalSection(ExtensibilityGlobals) = postSolution
83 | SolutionGuid = {44C4EBC2-31DC-4165-B8D7-17FE36C28405}
84 | EndGlobalSection
85 | EndGlobal
86 |
--------------------------------------------------------------------------------
/systemd/user/ss-uri-gen-chatbot-telegram.service:
--------------------------------------------------------------------------------
1 | [Unit]
2 | Description=Shadowsocks URI Generator Telegram Bot
3 | Wants=network-online.target
4 | After=network-online.target
5 |
6 | [Service]
7 | WorkingDirectory=%E/shadowsocks-uri-generator
8 | ExecStart=/usr/bin/ss-uri-gen-chatbot-telegram
9 |
10 | [Install]
11 | WantedBy=default.target
12 |
--------------------------------------------------------------------------------
/systemd/user/ss-uri-gen-server.service:
--------------------------------------------------------------------------------
1 | [Unit]
2 | Description=Shadowsocks URI Generator Server
3 | Wants=network-online.target
4 | After=network-online.target
5 |
6 | [Service]
7 | WorkingDirectory=%E/shadowsocks-uri-generator
8 | ExecStart=/usr/bin/ss-uri-gen-server
9 |
10 | [Install]
11 | WantedBy=default.target
12 |
--------------------------------------------------------------------------------
/systemd/user/ss-uri-gen.service:
--------------------------------------------------------------------------------
1 | [Unit]
2 | Description=Shadowsocks URI Generator
3 | Wants=network-online.target
4 | After=network-online.target
5 |
6 | [Service]
7 | WorkingDirectory=%E/shadowsocks-uri-generator
8 | ExecStart=/usr/bin/ss-uri-gen service
9 |
10 | [Install]
11 | WantedBy=default.target
12 |
--------------------------------------------------------------------------------