├── .gitattributes ├── ProxyStationService ├── host.json ├── Util │ ├── IEnvironmentManager.cs │ ├── IDownloader.cs │ ├── EnvironmentManager.cs │ ├── Yaml.cs │ ├── Misc.cs │ └── Downloader.cs ├── Model │ ├── ProfileType.cs │ ├── ShadowsocksRServer.cs │ ├── Server.cs │ ├── Profile.cs │ └── ShadowsocksServer.cs ├── ServerFilter │ ├── FilterFactory.cs │ ├── RegexFilter.cs │ ├── BaseFilter.cs │ └── NameFilter.cs ├── Constant.cs ├── Parser │ ├── NullParser.cs │ ├── SurgeListParser.cs │ ├── QuantumultXListParser.cs │ ├── ClashProxyProviderParser.cs │ ├── ParserFactory.cs │ ├── IProfileParser.cs │ ├── Template │ │ ├── Surge.cs │ │ └── QuantumultX.cs │ ├── GeneralParser.cs │ ├── ClashParser.cs │ ├── SurgeParser.cs │ └── QuantumultXParser.cs ├── TemplateFactory.cs ├── ProxyStation.csproj ├── ProfileFactory.cs └── HttpTrigger │ └── Functions.cs ├── .github └── workflows │ └── dotnet-test.yml ├── User-Agent.md ├── ProxyStationService.Tests ├── Parser │ ├── Fixtures │ │ ├── General.conf │ │ ├── Surge.conf │ │ ├── Demo.yaml │ │ └── Clash.yaml │ ├── SurgeParserTests.cs │ ├── GeneralParserTests.cs │ └── ClashParserTests.cs ├── ProxyStationService.Tests.csproj ├── ServerFilter │ ├── RegexFilterTests.cs │ └── NameFilterTests.cs ├── Util │ └── MiscTests.cs └── HttpTrigger │ ├── FunctionsTests.cs │ └── Fixtures.cs ├── LICENSE ├── ProxyStation.sln ├── README.md └── .gitignore /.gitattributes: -------------------------------------------------------------------------------- 1 | text eol=crlf -------------------------------------------------------------------------------- /ProxyStationService/host.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0" 3 | } -------------------------------------------------------------------------------- /ProxyStationService/Util/IEnvironmentManager.cs: -------------------------------------------------------------------------------- 1 | namespace ProxyStation.Util 2 | { 3 | public interface IEnvironmentManager 4 | { 5 | string Get(string key); 6 | } 7 | } -------------------------------------------------------------------------------- /ProxyStationService/Util/IDownloader.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.Extensions.Logging; 3 | 4 | namespace ProxyStation.Util 5 | { 6 | public interface IDownloader 7 | { 8 | Task Download(ILogger logger, string url); 9 | } 10 | } -------------------------------------------------------------------------------- /ProxyStationService/Util/EnvironmentManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ProxyStation.Util 4 | { 5 | public class EnvironmentManager : IEnvironmentManager 6 | { 7 | public string Get(string key) 8 | { 9 | return Environment.GetEnvironmentVariable(key); 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /ProxyStationService/Model/ProfileType.cs: -------------------------------------------------------------------------------- 1 | namespace ProxyStation.Model 2 | { 3 | public enum ProfileType 4 | { 5 | None, 6 | Original, 7 | Alias, 8 | General, 9 | Surge, 10 | SurgeList, 11 | Clash, 12 | ClashProxyProvider, 13 | QuantumultX, 14 | QuantumultXList, 15 | } 16 | } -------------------------------------------------------------------------------- /ProxyStationService/ServerFilter/FilterFactory.cs: -------------------------------------------------------------------------------- 1 | namespace ProxyStation.ServerFilter 2 | { 3 | public static class FilterFactory 4 | { 5 | public static BaseFilter GetFilter(string name) 6 | { 7 | return name switch 8 | { 9 | "name" => new NameFilter(), 10 | "regex" => new RegexFilter(), 11 | _ => null, 12 | }; 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /ProxyStationService/Constant.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ProxyStation 4 | { 5 | public static class Constant 6 | { 7 | public const string ObfsucationHost = "update.windows.com"; 8 | 9 | public const string DownloaderCacheBlobContainer = "downloader-cache"; 10 | 11 | public readonly static TimeSpan CacheOperationTimeout = TimeSpan.FromSeconds(5); 12 | 13 | public readonly static TimeSpan DownloadTimeout = TimeSpan.FromSeconds(10); 14 | } 15 | } -------------------------------------------------------------------------------- /ProxyStationService/Parser/NullParser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using ProxyStation.Model; 3 | 4 | namespace ProxyStation.ProfileParser 5 | { 6 | public class NullParser : IProfileParser 7 | { 8 | public string Encode(EncodeOptions options, Server[] servers, out Server[] encodedServers) => throw new NotImplementedException(); 9 | 10 | public string ExtName() => throw new NotImplementedException(); 11 | 12 | public Server[] Parse(string profile) => throw new NotImplementedException(); 13 | } 14 | } -------------------------------------------------------------------------------- /.github/workflows/dotnet-test.yml: -------------------------------------------------------------------------------- 1 | name: CI Test 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v2 12 | 13 | - name: Setup .NET Core 14 | uses: actions/setup-dotnet@v1 15 | with: 16 | dotnet-version: 3.1.100 17 | 18 | - name: Test 19 | run: dotnet test /p:CollectCoverage=true /p:CoverletOutputFormat=opencover 20 | 21 | - name: Upload coverage to Codecov 22 | uses: codecov/codecov-action@v1 23 | with: 24 | token: ${{ secrets.CODECOV_TOKEN }} 25 | fail_ci_if_error: true 26 | -------------------------------------------------------------------------------- /ProxyStationService/Model/ShadowsocksRServer.cs: -------------------------------------------------------------------------------- 1 | namespace ProxyStation.Model 2 | { 3 | public class ShadowsocksRServer : Server 4 | { 5 | public string Password { get; set; } 6 | 7 | public string Method { get; set; } 8 | 9 | public string Obfuscation { get; set; } 10 | 11 | public string ObfuscationParameter { get; set; } 12 | 13 | public string Protocol { get; set; } 14 | 15 | public string ProtocolParameter { get; set; } 16 | 17 | public int UDPPort { get; set; } 18 | 19 | public bool UDPOverTCP { get; set; } 20 | 21 | public override string ToString() => $"ShadowsocksR<{this.Host}:{this.Port}>"; 22 | } 23 | } -------------------------------------------------------------------------------- /User-Agent.md: -------------------------------------------------------------------------------- 1 | ### Surge 3 Mac 2 | * `Surge%203/863 CFNetwork/978.0.7 Darwin/18.6.0 (x86_64)` 3 | 4 | ### Surge 4 iOS 5 | * `Surge/1395 CFNetwork/1088.1 Darwin/19.0.0` 6 | 7 | ### Quantumult/619 8 | * `Quantumult/619 CFNetwork/1088.1 Darwin/19.0.0` 9 | 10 | ### Shadowrocket 11 | * `Mozilla/5.0 (iPad; CPU OS 5_0 like Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko) Version/5.1 Mobile/9A334 Safari/7534.48.3` 12 | 13 | ### Clash for Windows 14 | * `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) clash_win/0.6.0 Chrome/73.0.3683.119 Electron/5.0.0 Safari/537.36` 15 | 16 | ### ClashX 17 | * `ClashX/1.10.0 (com.west2online.ClashX; build:1.10.0; OS X 10.14.5) Alamofire/4.8.2` 18 | -------------------------------------------------------------------------------- /ProxyStationService/Model/Server.cs: -------------------------------------------------------------------------------- 1 | namespace ProxyStation.Model 2 | { 3 | public abstract class Server 4 | { 5 | public string Name { get; set; } 6 | 7 | public string Host { get; set; } 8 | 9 | public int Port { get; set; } 10 | } 11 | 12 | public enum SnellObfuscationMode 13 | { 14 | None, 15 | HTTP, 16 | TLS, 17 | } 18 | 19 | public class SnellServer : Server 20 | { 21 | public string Password { get; set; } 22 | 23 | public SnellObfuscationMode ObfuscationMode { get; set; } 24 | 25 | public string ObfuscationHost { get; set; } 26 | 27 | public bool FastOpen { get; set; } 28 | } 29 | 30 | } -------------------------------------------------------------------------------- /ProxyStationService/TemplateFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using ProxyStation.Util; 3 | 4 | namespace ProxyStation 5 | { 6 | public static class TemplateFactory 7 | { 8 | static IEnvironmentManager environmentManager; 9 | 10 | public static void SetEnvironmentManager(IEnvironmentManager environmentManager) 11 | { 12 | TemplateFactory.environmentManager = environmentManager; 13 | } 14 | 15 | public static string GetTemplateUrl(string templateName) 16 | { 17 | var data = TemplateFactory.environmentManager.Get("Template" + Misc.KebabCase2PascalCase(templateName.ToLower())); 18 | return data; 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /ProxyStationService/Parser/SurgeListParser.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using ProxyStation.Model; 3 | 4 | namespace ProxyStation.ProfileParser 5 | { 6 | public class SurgeListParser : IProfileParser 7 | { 8 | private ILogger logger; 9 | 10 | public SurgeListParser(ILogger logger) 11 | { 12 | this.logger = logger; 13 | } 14 | 15 | public Server[] Parse(string profile) => new SurgeParser(logger).ParseProxyList(profile); 16 | 17 | public string Encode(EncodeOptions options, Server[] servers, out Server[] encodedServers) 18 | => new SurgeParser(logger).EncodeProxyList(servers, out encodedServers); 19 | 20 | public string ExtName() => ""; 21 | } 22 | } -------------------------------------------------------------------------------- /ProxyStationService.Tests/Parser/Fixtures/General.conf: -------------------------------------------------------------------------------- 1 | c3NyOi8vTVRJM0xqQXVNQzR4T2pFeU16UTZZWFYwYUY5aFpYTXhNamhmYldRMU9tRmxjeTB4TWpndFkyWmlPblJzY3pFdU1sOTBhV05yWlhSZllYVjBhRHBaVjBab1dXMUthUzhfYjJKbWMzQmhjbUZ0UFZsdVNteFpWM1F6V1ZSRmVFeHRNWFphVVNaeVpXMWhjbXR6UFRWeVYwdzJTeTFXTlV4cGREVndZVWcKc3NyOi8vTVRJM0xqQXVNQzR4T2pFeU16UTZZWFYwYUY5aFpYTXhNamhmYldRMU9tRmxjeTB4TWpndFkyWmlPblJzY3pFdU1sOTBhV05yWlhSZllYVjBhRHBaVjBab1dXMUthUzhfYjJKbWMzQmhjbUZ0UFZsdVNteFpWM1F6V1ZSRmVFeHRNWFphVVEKc3M6Ly9ZV1Z6TFRFeU9DMW5ZMjA2ZEdWemRBPT1AMTkyLjE2OC4xMDAuMTo4ODg4I0V4YW1wbGUxCnNzOi8vY21NMExXMWtOVHB3WVhOemQyUT1AMTkyLjE2OC4xMDAuMTo4ODg4Lz9wbHVnaW49b2Jmcy1sb2NhbCUzQm9iZnMlM0RodHRwI0V4YW1wbGUyCnNzOi8vWW1ZdFkyWmlPblJsYzNRQDE5Mi4xNjguMTAwLjE6ODg4OC8/cGx1Z2luPXVybC1lbmNvZGVkLXBsdWdpbi1hcmd1bWVudC12YWx1ZSZ1bnN1cHBvcnRlZC1hcmd1bWVudHM9c2hvdWxkLWJlLWlnbm9yZWQjRHVtbXkrcHJvZmlsZStuYW1l -------------------------------------------------------------------------------- /ProxyStationService/ServerFilter/RegexFilter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.RegularExpressions; 3 | using Microsoft.Extensions.Logging; 4 | using ProxyStation.Model; 5 | using ProxyStation.Util; 6 | using YamlDotNet.RepresentationModel; 7 | 8 | namespace ProxyStation.ServerFilter 9 | { 10 | public class RegexFilter : BaseFilter 11 | { 12 | Regex regex; 13 | 14 | public override void LoadOptions(YamlNode node, ILogger logger) 15 | { 16 | var pattern = Yaml.GetStringOrDefaultFromYamlChildrenNode(node, "pattern"); 17 | this.regex = new Regex(pattern); 18 | } 19 | 20 | public override bool Match(Server server, ILogger logger) 21 | { 22 | var match = this.regex.Match(server.Name); 23 | return match.Success; 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /ProxyStationService/Parser/QuantumultXListParser.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using ProxyStation.Model; 3 | using ProxyStation.Util; 4 | 5 | namespace ProxyStation.ProfileParser 6 | { 7 | public class QuantumultXListParser : IProfileParser 8 | { 9 | private readonly ILogger logger; 10 | 11 | private readonly QuantumultXParser parentParser; 12 | 13 | public QuantumultXListParser(ILogger logger, IDownloader downloader) 14 | { 15 | this.logger = logger; 16 | this.parentParser = new QuantumultXParser(logger, downloader); 17 | } 18 | 19 | public string Encode(EncodeOptions options, Server[] servers, out Server[] encodedServers) 20 | => this.parentParser.EncodeProxyList(servers, out encodedServers); 21 | 22 | public Server[] Parse(string profile) => this.parentParser.ParseProxyList(profile); 23 | 24 | public string ExtName() => ".conf"; 25 | } 26 | } -------------------------------------------------------------------------------- /ProxyStationService/Model/Profile.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics.CodeAnalysis; 4 | using System.Threading.Tasks; 5 | using Microsoft.Extensions.Logging; 6 | using ProxyStation.ServerFilter; 7 | using ProxyStation.Util; 8 | 9 | namespace ProxyStation.Model 10 | { 11 | public class Profile : IEquatable 12 | { 13 | public string Source { get; set; } 14 | 15 | public ProfileType Type { get; set; } 16 | 17 | public string Name { get; set; } 18 | 19 | public bool AllowDirectAccess { get; set; } 20 | 21 | public List Filters { get; set; } = new List(); 22 | 23 | public async Task Download(ILogger logger, IDownloader downloader) 24 | { 25 | return await downloader.Download(logger, this.Source); 26 | } 27 | 28 | public bool Equals([AllowNull] Profile other) 29 | { 30 | return other != null && other.Name == this.Name; 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /ProxyStationService.Tests/ProxyStationService.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1 5 | false 6 | 7 | 8 | 9 | 10 | runtime; build; native; contentfiles; analyzers; buildtransitive 11 | all 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Tony Ke 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /ProxyStationService/ProxyStation.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | netcoreapp3.1 4 | v3 5 | ProxyStation 6 | 7 | 8 | 9 | 10 | 11 | 12 | runtime; build; native; contentfiles; analyzers; buildtransitive 13 | all 14 | 15 | 16 | 17 | 18 | 19 | PreserveNewest 20 | 21 | 22 | PreserveNewest 23 | Never 24 | 25 | 26 | -------------------------------------------------------------------------------- /ProxyStationService/Util/Yaml.cs: -------------------------------------------------------------------------------- 1 | using YamlDotNet.RepresentationModel; 2 | 3 | namespace ProxyStation.Util { 4 | public class Yaml { 5 | public static bool TryGetStringFromYamlChildrenNode(YamlNode rootNode, string keyName, out string buffer) 6 | { 7 | buffer = ""; 8 | if (!(rootNode as YamlMappingNode).Children.ContainsKey(keyName)) return false; 9 | 10 | var node = (rootNode as YamlMappingNode).Children[keyName]; 11 | if (!(node is YamlScalarNode)) return false; 12 | 13 | buffer = (node as YamlScalarNode).Value; 14 | return true; 15 | } 16 | 17 | public static string GetStringOrDefaultFromYamlChildrenNode(YamlNode rootNode, string keyName, string defaultValue = "") 18 | { 19 | if (!(rootNode as YamlMappingNode).Children.ContainsKey(keyName)) return defaultValue; 20 | 21 | var node = (rootNode as YamlMappingNode).Children[keyName]; 22 | if (!(node is YamlScalarNode)) return defaultValue; 23 | 24 | return (node as YamlScalarNode).Value; 25 | } 26 | 27 | public static bool GetTruthFromYamlChildrenNode(YamlNode rootNode, string keyName) 28 | { 29 | if (!(rootNode as YamlMappingNode).Children.ContainsKey(keyName)) return default; 30 | 31 | var node = (rootNode as YamlMappingNode).Children[keyName]; 32 | if (!(node is YamlScalarNode)) return default; 33 | 34 | return (node as YamlScalarNode).Value == "true"; 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /ProxyStationService/ServerFilter/BaseFilter.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using Microsoft.Extensions.Logging; 3 | using ProxyStation.Model; 4 | using ProxyStation.Util; 5 | using YamlDotNet.RepresentationModel; 6 | 7 | namespace ProxyStation.ServerFilter 8 | { 9 | public enum FilterMode 10 | { 11 | WhiteList, 12 | BlackList, 13 | } 14 | 15 | public abstract class BaseFilter 16 | { 17 | public string Name { get; set; } 18 | 19 | public FilterMode Mode { get; set; } 20 | 21 | public abstract bool Match(Server server, ILogger logger); 22 | 23 | public virtual void LoadOptions(YamlNode node, ILogger logger) 24 | { 25 | Mode = (Yaml.GetStringOrDefaultFromYamlChildrenNode(node, "mode").ToLower()) switch 26 | { 27 | "whitelist" => FilterMode.WhiteList, 28 | "blacklist" => FilterMode.BlackList, 29 | _ => FilterMode.BlackList, 30 | }; 31 | } 32 | 33 | public Server[] Do(Server[] servers, ILogger logger) 34 | { 35 | return servers 36 | .Where(s => 37 | { 38 | var match = this.Match(s, logger); 39 | var keep = match == (this.Mode == FilterMode.WhiteList); 40 | if (!keep) 41 | { 42 | logger.LogDebug($"[{{ComponentName}}] Server {s.Name} is removed.", this.GetType().ToString()); 43 | } 44 | return keep; 45 | }) 46 | .ToArray(); 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /ProxyStationService/Parser/ClashProxyProviderParser.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using System.Linq; 4 | using Microsoft.Extensions.Logging; 5 | using ProxyStation.Model; 6 | using ProxyStation.Util; 7 | using YamlDotNet.RepresentationModel; 8 | using YamlDotNet.Serialization; 9 | 10 | namespace ProxyStation.ProfileParser 11 | { 12 | public class ClashProxyProviderParser : IProfileParser 13 | { 14 | private ILogger logger; 15 | 16 | private ClashParser parentParser; 17 | 18 | public ClashProxyProviderParser(ILogger logger) 19 | { 20 | this.logger = logger; 21 | this.parentParser = new ClashParser(logger); 22 | } 23 | 24 | public string Encode(EncodeOptions options, Server[] servers, out Server[] encodedServers) 25 | { 26 | var rootElement = new Dictionary(); 27 | var proxyServers = this.parentParser.InternalEncode(options, servers, out encodedServers); 28 | rootElement.Add("proxies", proxyServers); 29 | return new SerializerBuilder().Build().Serialize(rootElement); 30 | } 31 | 32 | public Server[] Parse(string profile) 33 | { 34 | using (var reader = new StringReader(profile)) 35 | { 36 | var yaml = new YamlStream(); 37 | yaml.Load(reader); 38 | 39 | var proxyNode = (yaml.Documents[0].RootNode as YamlMappingNode).Children["proxies"]; 40 | return this.parentParser.Parse(proxyNode); 41 | } 42 | } 43 | 44 | public string ExtName() => ".yaml"; 45 | } 46 | } -------------------------------------------------------------------------------- /ProxyStationService.Tests/ServerFilter/RegexFilterTests.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using Microsoft.Extensions.Logging; 3 | using ProxyStation.Model; 4 | using ProxyStation.ServerFilter; 5 | using Xunit; 6 | using Xunit.Abstractions; 7 | using YamlDotNet.RepresentationModel; 8 | 9 | namespace ProxyStation.Tests.ServerFilter 10 | { 11 | public class RegexFilterTests 12 | { 13 | readonly RegexFilter filter; 14 | 15 | readonly Server[] servers; 16 | 17 | readonly ILogger logger; 18 | 19 | public RegexFilterTests(ITestOutputHelper output) 20 | { 21 | this.logger = output.BuildLogger(); 22 | this.filter = new RegexFilter(); 23 | this.servers = new Server[]{ 24 | new ShadowsocksServer() { Name = "goodserver 1" }, 25 | new ShadowsocksServer() { Name = "goodserver 2" }, 26 | new ShadowsocksServer() { Name = "goodserver 3" }, 27 | new ShadowsocksServer() { Name = "badserver 1" }, 28 | new ShadowsocksServer() { Name = "badserver 2" }, 29 | }; 30 | } 31 | 32 | [Fact] 33 | public void ShouldSuccessWithWhitelistMode() 34 | { 35 | var optionsYaml = @" 36 | name: regex 37 | mode: whitelist 38 | pattern: bad.*? 39 | "; 40 | var yaml = new YamlStream(); 41 | yaml.Load(new StringReader(optionsYaml)); 42 | 43 | filter.LoadOptions(yaml.Documents[0].RootNode, this.logger); 44 | Assert.Equal(FilterMode.WhiteList, filter.Mode); 45 | 46 | var filtered = filter.Do(this.servers, this.logger); 47 | Assert.Equal(2, filtered.Length); 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /ProxyStationService/ServerFilter/NameFilter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Microsoft.Extensions.Logging; 4 | using ProxyStation.Model; 5 | using ProxyStation.Util; 6 | using YamlDotNet.RepresentationModel; 7 | 8 | namespace ProxyStation.ServerFilter 9 | { 10 | public enum NameFilterMatching 11 | { 12 | HasPrefix, 13 | HasSuffix, 14 | Contains, 15 | } 16 | 17 | public class NameFilter : BaseFilter 18 | { 19 | public string Keyword { get; set; } 20 | 21 | public NameFilterMatching Matching { get; set; } 22 | 23 | public override bool Match(Server server, ILogger logger) 24 | { 25 | return this.Matching switch 26 | { 27 | NameFilterMatching.HasPrefix => server.Name.StartsWith(Keyword), 28 | NameFilterMatching.HasSuffix => server.Name.EndsWith(Keyword), 29 | NameFilterMatching.Contains => server.Name.Contains(Keyword), 30 | _ => throw new NotImplementedException(), 31 | }; 32 | } 33 | 34 | public override void LoadOptions(YamlNode node, ILogger logger) 35 | { 36 | base.LoadOptions(node, logger); 37 | if (node == null || node.NodeType != YamlNodeType.Mapping) return; 38 | 39 | Keyword = Yaml.GetStringOrDefaultFromYamlChildrenNode(node, "keyword"); 40 | Matching = (Yaml.GetStringOrDefaultFromYamlChildrenNode(node, "matching")) switch 41 | { 42 | "prefix" => NameFilterMatching.HasPrefix, 43 | "suffix" => NameFilterMatching.HasSuffix, 44 | "contains" => NameFilterMatching.Contains, 45 | _ => NameFilterMatching.Contains, 46 | }; 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /ProxyStationService/Parser/ParserFactory.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using ProxyStation.Model; 3 | using ProxyStation.Util; 4 | 5 | namespace ProxyStation.ProfileParser 6 | { 7 | public static class ParserFactory 8 | { 9 | public static IProfileParser GetParser(ProfileType type, ILogger logger, IDownloader downloader) 10 | { 11 | return type switch 12 | { 13 | ProfileType.Original => new NullParser(), 14 | ProfileType.General => new GeneralParser(logger), 15 | ProfileType.Surge => new SurgeParser(logger), 16 | ProfileType.SurgeList => new SurgeListParser(logger), 17 | ProfileType.Clash => new ClashParser(logger), 18 | ProfileType.ClashProxyProvider => new ClashProxyProviderParser(logger), 19 | ProfileType.QuantumultX => new QuantumultXParser(logger, downloader), 20 | ProfileType.QuantumultXList => new QuantumultXListParser(logger, downloader), 21 | _ => null, 22 | }; 23 | } 24 | 25 | public static IProfileParser GetParser(string type, ILogger logger, IDownloader downloader) 26 | { 27 | var profileType = (type.ToLower()) switch 28 | { 29 | "original" => ProfileType.Original, 30 | "general" => ProfileType.General, 31 | "surge" => ProfileType.Surge, 32 | "surge-list" => ProfileType.SurgeList, 33 | "clash" => ProfileType.Clash, 34 | "clash-proxy-provider" => ProfileType.ClashProxyProvider, 35 | "quantumult-x" => ProfileType.QuantumultX, 36 | "quantumult-x-list" => ProfileType.QuantumultXList, 37 | _ => ProfileType.None, 38 | }; 39 | 40 | if (profileType == ProfileType.None) 41 | { 42 | return null; 43 | } 44 | 45 | return ParserFactory.GetParser(profileType, logger, downloader); 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /ProxyStation.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ProxyStation", "ProxyStationService\ProxyStation.csproj", "{4E62EA9E-A65A-4189-BE9B-89436636F723}" 5 | EndProject 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ProxyStationService.Tests", "ProxyStationService.Tests\ProxyStationService.Tests.csproj", "{87972193-813F-4DC9-B009-58040E37E9C2}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {312F7A1D-63F1-42E9-B6CE-36CF450CEF6F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {312F7A1D-63F1-42E9-B6CE-36CF450CEF6F}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {312F7A1D-63F1-42E9-B6CE-36CF450CEF6F}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {312F7A1D-63F1-42E9-B6CE-36CF450CEF6F}.Release|Any CPU.Build.0 = Release|Any CPU 18 | {42D8FC55-29D1-4800-B284-F58FE8980992}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 19 | {42D8FC55-29D1-4800-B284-F58FE8980992}.Debug|Any CPU.Build.0 = Debug|Any CPU 20 | {42D8FC55-29D1-4800-B284-F58FE8980992}.Release|Any CPU.ActiveCfg = Release|Any CPU 21 | {42D8FC55-29D1-4800-B284-F58FE8980992}.Release|Any CPU.Build.0 = Release|Any CPU 22 | {4E62EA9E-A65A-4189-BE9B-89436636F723}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 23 | {4E62EA9E-A65A-4189-BE9B-89436636F723}.Debug|Any CPU.Build.0 = Debug|Any CPU 24 | {4E62EA9E-A65A-4189-BE9B-89436636F723}.Release|Any CPU.ActiveCfg = Release|Any CPU 25 | {4E62EA9E-A65A-4189-BE9B-89436636F723}.Release|Any CPU.Build.0 = Release|Any CPU 26 | {87972193-813F-4DC9-B009-58040E37E9C2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {87972193-813F-4DC9-B009-58040E37E9C2}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {87972193-813F-4DC9-B009-58040E37E9C2}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {87972193-813F-4DC9-B009-58040E37E9C2}.Release|Any CPU.Build.0 = Release|Any CPU 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /ProxyStationService/Parser/IProfileParser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using ProxyStation.Model; 3 | using ProxyStation.Util; 4 | 5 | namespace ProxyStation.ProfileParser 6 | { 7 | public class EncodeOptions 8 | { 9 | /// 10 | /// Name of the profile to be encoded 11 | /// 12 | /// 13 | public string ProfileName { get; set; } 14 | 15 | /// 16 | /// The template to be used to generate a profile 17 | /// 18 | /// 19 | public string Template { get; set; } 20 | 21 | /// 22 | /// The downloader used to download file 23 | /// 24 | /// 25 | public IDownloader Downloader { get; set; } 26 | } 27 | 28 | public interface IProfileParser 29 | { 30 | /// 31 | /// Parse profile and returns a list of proxy servers 32 | /// 33 | /// profile content 34 | /// a list of servers 35 | Server[] Parse(string profile); 36 | 37 | /// 38 | /// Combine and format the proxy servers and some snippets to generate a profile for proxy software 39 | /// 40 | /// encode options 41 | /// a list of proxy servers 42 | /// a list of encoded proxy servers 43 | /// the content of profile 44 | string Encode(EncodeOptions options, Server[] servers, out Server[] encodedServers); 45 | 46 | /// 47 | /// Return extension name of a profile file 48 | /// 49 | /// extension name like `.json` 50 | string ExtName(); 51 | } 52 | 53 | public class InvalidTemplateException : Exception 54 | { 55 | } 56 | 57 | public class ParseException : Exception 58 | { 59 | private readonly string reason; 60 | private readonly string rawURI; 61 | 62 | public ParseException(string reason, string rawURI) 63 | { 64 | this.reason = reason; 65 | this.rawURI = rawURI; 66 | } 67 | } 68 | } -------------------------------------------------------------------------------- /ProxyStationService/Parser/Template/Surge.cs: -------------------------------------------------------------------------------- 1 | namespace ProxyStation.ProfileParser.Template 2 | { 3 | public static class Surge 4 | { 5 | public const string ServerListPlaceholder = "{% SERVER_LIST %}"; 6 | 7 | public const string ServerNamesPlaceholder = "{% SERVER_NAMES %}"; 8 | 9 | public readonly static string Template = $@"[General] 10 | loglevel = notify 11 | dns-server = system, 223.5.5.5, 223.6.6.6, 8.8.8.8, 8.8.4.4 12 | skip-proxy = 127.0.0.1, 192.168.0.0/16, 10.0.0.0/8, 172.16.0.0/12, 100.64.0.0/10, 17.0.0.0/8, localhost, *.local, *.crashlytics.com 13 | external-controller-access = best-proxy-staion@0.0.0.0:6170 14 | allow-wifi-access = true 15 | interface = 0.0.0.0 16 | socks-interface = 0.0.0.0 17 | port = 8888 18 | socks-port = 8889 19 | enhanced-mode-by-rule = false 20 | show-error-page-for-reject = true 21 | exclude-simple-hostnames = true 22 | ipv6 = true 23 | replica = false 24 | network-framework = true 25 | 26 | [Replica] 27 | hide-apple-request = true 28 | hide-crashlytics-request = true 29 | hide-udp = false 30 | use-keyword-filter = false 31 | 32 | [Proxy] 33 | {Surge.ServerListPlaceholder} 34 | 35 | [Proxy Group] 36 | Global Traffic = select, Auto Proxy, DIRECT 37 | CN Traffic = select, DIRECT, Auto Proxy 38 | Default = select, Auto Proxy, DIRECT 39 | AdBlock = select, REJECT, REJECT-TINYGIF, Global Traffic, CN Traffic 40 | Auto Proxy = url-test, {Surge.ServerNamesPlaceholder}, url=http://captive.apple.com, interval=600, tolerance=200 41 | 42 | [Rule] 43 | RULE-SET,https://raw.githubusercontent.com/rixCloud-Inc/rixCloud-Surge3_Rules/master/Anti-GFW%2B.list,Global Traffic 44 | RULE-SET,https://raw.githubusercontent.com/rixCloud-Inc/rixCloud-Surge3_Rules/master/Anti-GFW.list,Global Traffic 45 | RULE-SET,https://raw.githubusercontent.com/rixCloud-Inc/rixCloud-Surge3_Rules/master/Stream.list,Global Traffic 46 | RULE-SET,https://raw.githubusercontent.com/rixCloud-Inc/rixCloud-Surge3_Rules/master/Apple.list,Global Traffic 47 | RULE-SET,https://raw.githubusercontent.com/rixCloud-Inc/rixCloud-Surge3_Rules/master/Reject.list, AdBlock 48 | RULE-SET,https://raw.githubusercontent.com/rixCloud-Inc/rixCloud-Surge3_Rules/master/Reject-GIF.list, AdBlock 49 | RULE-SET,https://raw.githubusercontent.com/rixCloud-Inc/rixCloud-Surge3_Rules/master/China.list,CN Traffic 50 | RULE-SET,https://raw.githubusercontent.com/rixCloud-Inc/rixCloud-Surge3_Rules/master/Netease_Music.list,CN Traffic 51 | GEOIP,CN,CN Traffic 52 | FINAL,Default,dns-failed 53 | "; 54 | } 55 | 56 | } -------------------------------------------------------------------------------- /ProxyStationService.Tests/Parser/Fixtures/Surge.conf: -------------------------------------------------------------------------------- 1 | [General] 2 | loglevel = notify 3 | bypass-system = true 4 | skip-proxy = 127.0.0.0/24,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,100.64.0.0/10,17.0.0.0/8,localhost,*.local,169.254.0.0/16,224.0.0.0/4,240.0.0.0/4 5 | bypass-tun = 127.0.0.0/24,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,100.64.0.0/10,17.0.0.0/8,localhost,*.local,169.254.0.0/16,224.0.0.0/4,240.0.0.0/4 6 | dns-server = system, 114.114.114.114, 119.29.29.29, 223.5.5.5, 8.8.8.8, 8.8.4.4 7 | replica = false 8 | enhanced-mode-by-rule = true 9 | ipv6 = false 10 | exclude-simple-hostnames = true 11 | test-timeout = 5 12 | allow-wifi-access = false 13 | 14 | [Replica] 15 | hide-apple-request=true 16 | 17 | [Proxy] 18 | sadfasd = ss, 12381293, 123, encrypt-method=aes-128-gcm, password=1231341, obfs=http, obfs-host=2341324124, udp-relay=true, interface=asdasd, allow-other-interface=true, tfo=true 19 | 香港高级 BGP 中继 5 = custom, hk5.edge.iplc.app, 155, rc4-md5, asdads, http://example.com/, udp-relay=true 20 | 🇭🇰 中国杭州 -> 香港 01 | IPLC = ss, 123.123.123.123, 10086, encrypt-method=xchacha20-ietf-poly1305, password=gasdas, obfs=tls, obfs-host=download.windowsupdate.com, udp-relay=true 21 | sadfasd=custom, 12381293, 123, encrypt-method=aes-128-gcm, password=1231341, obfs=http, obfs-host=2341324124, udp-relay=true, interface=asdasd, allow-other-interface=true, tfo=true 22 | 23 | [Proxy Group] 24 | AUTO = url-test, 香港高级 BGP 中继 1, 香港高级 BGP 中继 2, 香港高级 BGP 中继 3, 香港高级 BGP 中继 4, 香港高级 BGP 中继 5, 香港高级 BGP 中继 6, 香港高级 BGP 中继 7, 香港高级 BGP 中继 8, 香港高级 BGP 中继 9, 香港高级 BGP 中继 10, 香港高级 BGP 中继 11, 香港高级 BGP 中继 12, 香港高级 BGP 中继 13, 香港高级 BGP 中继 14, 香港高级 BGP 中继 15, 香港高级 BGP 中继 16, 香港 BGP 中继 1, 香港 BGP 中继 2, 香港 BGP 中继 3, 香港 BGP 中继 4, 香港 BGP 中继 5, 香港 BGP 中继 6, 香港 BGP 中继 7, 香港 BGP 中继 8, 香港 BGP 中继 9, 香港 BGP 中继 10, 香港 BGP 中继 11, 香港 BGP 中继 12, 香港 BGP 中继 13, 香港 BGP 中继 14, 香港 BGP 中继 15, 香港 BGP 中继 16, 香港标准 BGP 中继 1, 香港标准 BGP 中继 2, 香港标准 BGP 中继 3, 香港标准 BGP 中继 4, 香港标准 BGP 中继 5, url=http://www.gstatic.com/generate_204, interval=180, timeout=1, tolerance=100 25 | RIXCLOUD = select, AUTO, DIRECT, 香港高级 BGP 中继 1, 香港高级 BGP 中继 2, 香港高级 BGP 中继 3, 香港高级 BGP 中继 4, 香港高级 BGP 中继 5, 香港高级 BGP 中继 6, 香港高级 BGP 中继 7, 香港高级 BGP 中继 8, 香港高级 BGP 中继 9, 香港高级 BGP 中继 10, 香港高级 BGP 中继 11, 香港高级 BGP 中继 12, 香港高级 BGP 中继 13, 香港高级 BGP 中继 14, 香港高级 BGP 中继 15, 香港高级 BGP 中继 16, 香港 BGP 中继 1, 香港 BGP 中继 2, 香港 BGP 中继 3, 香港 BGP 中继 4, 香港 BGP 中继 5, 香港 BGP 中继 6, 香港 BGP 中继 7, 香港 BGP 中继 8, 香港 BGP 中继 9, 香港 BGP 中继 10, 香港 BGP 中继 11, 香港 BGP 中继 12, 香港 BGP 中继 13, 香港 BGP 中继 14, 香港 BGP 中继 15, 香港 BGP 中继 16, 香港标准 BGP 中继 1, 香港标准 BGP 中继 2, 香港标准 BGP 中继 3, 香港标准 BGP 中继 4, 香港标准 BGP 中继 5 26 | 27 | [Rule] 28 | GEOIP,CN,DIRECT 29 | FINAL,RIXCLOUD,dns-failed 30 | -------------------------------------------------------------------------------- /ProxyStationService/ProfileFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using Microsoft.Extensions.Logging; 4 | using ProxyStation.Model; 5 | using ProxyStation.ServerFilter; 6 | using ProxyStation.Util; 7 | using YamlDotNet.RepresentationModel; 8 | 9 | namespace ProxyStation 10 | { 11 | public static class ProfileFactory 12 | { 13 | static IEnvironmentManager environmentManager; 14 | 15 | public static void SetEnvironmentManager(IEnvironmentManager environmentManager) 16 | { 17 | ProfileFactory.environmentManager = environmentManager; 18 | } 19 | 20 | public static ProfileType ParseProfileTypeName(string profileType) 21 | { 22 | return (profileType.ToLower()) switch 23 | { 24 | "general" => ProfileType.General, 25 | "surge" => ProfileType.Surge, 26 | "clash" => ProfileType.Clash, 27 | "clash-proxy-provider" => ProfileType.ClashProxyProvider, 28 | "surge-list" => ProfileType.SurgeList, 29 | "alias" => ProfileType.Alias, 30 | _ => ProfileType.None, 31 | }; 32 | } 33 | 34 | public static Profile Get(string profileName, ILogger logger) 35 | { 36 | var data = ProfileFactory.environmentManager.Get(profileName); 37 | if (String.IsNullOrEmpty(data)) return null; 38 | 39 | var profile = new Profile(); 40 | using (var reader = new StringReader(data)) 41 | { 42 | var yaml = new YamlStream(); 43 | yaml.Load(reader); 44 | var mapping = (yaml.Documents[0].RootNode as YamlMappingNode).Children; 45 | 46 | profile.Source = (mapping["source"] as YamlScalarNode).Value; 47 | profile.Name = (mapping["name"] as YamlScalarNode).Value; 48 | profile.Type = ParseProfileTypeName((mapping["type"] as YamlScalarNode).Value); 49 | profile.AllowDirectAccess = Yaml.GetTruthFromYamlChildrenNode(yaml.Documents[0].RootNode, "allowDirectAccess"); 50 | 51 | YamlNode filtersNode; 52 | if (!mapping.TryGetValue("filters", out filtersNode) || filtersNode.NodeType != YamlNodeType.Sequence) 53 | { 54 | goto EndReading; 55 | } 56 | 57 | foreach (var filterNode in (filtersNode as YamlSequenceNode).Children) 58 | { 59 | if (filterNode.NodeType != YamlNodeType.Mapping) continue; 60 | var filter = FilterFactory.GetFilter(Yaml.GetStringOrDefaultFromYamlChildrenNode(filterNode, "name")); 61 | if (filter == null) continue; 62 | filter.LoadOptions(filterNode, logger); 63 | profile.Filters.Add(filter); 64 | } 65 | } 66 | 67 | EndReading: 68 | return profile; 69 | } 70 | } 71 | } -------------------------------------------------------------------------------- /ProxyStationService/Model/ShadowsocksServer.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace ProxyStation.Model 4 | { 5 | public abstract class SSPluginOptions { } 6 | 7 | public enum SimpleObfsPluginMode 8 | { 9 | HTTP, 10 | 11 | TLS, 12 | } 13 | 14 | public class SimpleObfsPluginOptions : SSPluginOptions 15 | { 16 | public SimpleObfsPluginMode Mode { get; set; } 17 | 18 | public string Host { get; set; } 19 | 20 | public string Uri { get; set; } 21 | 22 | public static bool TryParseMode(string value, out SimpleObfsPluginMode mode) 23 | { 24 | mode = default; 25 | 26 | if (string.IsNullOrWhiteSpace(value)) 27 | { 28 | return false; 29 | } 30 | 31 | switch (value.Trim().ToLower()) 32 | { 33 | case "tls": 34 | mode = SimpleObfsPluginMode.TLS; 35 | break; 36 | case "http": 37 | mode = SimpleObfsPluginMode.HTTP; 38 | break; 39 | default: 40 | return false; 41 | }; 42 | return true; 43 | } 44 | } 45 | 46 | public enum V2RayPluginMode 47 | { 48 | WebSocket, 49 | 50 | QUIC, 51 | } 52 | 53 | public class V2RayPluginOptions : SSPluginOptions 54 | { 55 | public V2RayPluginMode Mode { get; set; } 56 | 57 | public string Host { get; set; } 58 | 59 | public string Path { get; set; } 60 | 61 | public bool EnableTLS { get; set; } 62 | 63 | public bool SkipCertVerification { get; set; } 64 | 65 | public Dictionary Headers { get; set; } 66 | 67 | public static bool TryParseMode(string value, out V2RayPluginMode mode) 68 | { 69 | mode = default; 70 | 71 | if (string.IsNullOrWhiteSpace(value)) 72 | { 73 | return false; 74 | } 75 | 76 | switch (value.Trim().ToLower()) 77 | { 78 | case "ws": 79 | mode = V2RayPluginMode.WebSocket; 80 | break; 81 | case "websocket": 82 | mode = V2RayPluginMode.WebSocket; 83 | break; 84 | case "quic": 85 | mode = V2RayPluginMode.QUIC; 86 | break; 87 | default: 88 | return false; 89 | 90 | }; 91 | return true; 92 | } 93 | } 94 | 95 | public class ShadowsocksServer : Server 96 | { 97 | public string Password { get; set; } 98 | 99 | public string Method { get; set; } 100 | 101 | public bool UDPRelay { get; set; } 102 | 103 | public bool FastOpen { get; set; } 104 | 105 | public SSPluginOptions PluginOptions { get; set; } 106 | 107 | public override string ToString() => $"Shadowsocks<{this.Host}:{this.Port}>"; 108 | } 109 | } -------------------------------------------------------------------------------- /ProxyStationService/Parser/Template/QuantumultX.cs: -------------------------------------------------------------------------------- 1 | namespace ProxyStation.ProfileParser.Template 2 | { 3 | public static class QuantumultX 4 | { 5 | public const string ServerListUrlPlaceholder = "{% SERVER_LIST_URL %}"; 6 | 7 | public readonly static string Template = $@"[general] 8 | excluded_routes = 192.168.0.0/16, 10.0.0.0/8, 172.16.0.0/12, 100.64.0.0/10, 17.0.0.0/8 9 | network_check_url = http://www.gstatic.com/generate_204 10 | server_check_url = http://www.gstatic.cn/generate_204 11 | geo_location_checker = http://ip-api.com/json/?lang=zh-CN, https://github.com/KOP-XIAO/QuantumultX/raw/master/Scripts/IP_API.js 12 | 13 | [dns] 14 | server = 119.29.29.29 15 | server = 119.28.28.28 16 | server = 1.2.4.8 17 | server = 182.254.116.116 18 | 19 | [policy] 20 | static=ADBlock, reject, direct, Auto Proxy 21 | static=CN Traffic, direct, Auto Proxy 22 | static=Global Traffic, Auto Proxy, direct, Proxy 23 | static=Default, Global Traffic, CN Traffic 24 | 25 | [server_remote] 26 | {ServerListUrlPlaceholder}, as-policy=available, tag=Auto Proxy 27 | 28 | [filter_remote] 29 | https://raw.githubusercontent.com/GeQ1an/Rules/master/QuantumultX/Filter/AdBlock.list, tag=AdBlock, force-policy=ADBlock 30 | https://raw.githubusercontent.com/GeQ1an/Rules/master/QuantumultX/Filter/Netflix.list, tag=Netflix, force-policy=Global Traffic 31 | https://raw.githubusercontent.com/GeQ1an/Rules/master/QuantumultX/Filter/YouTube.list, tag=YouTube, force-policy=Global Traffic 32 | https://raw.githubusercontent.com/GeQ1an/Rules/master/QuantumultX/Filter/Microsoft.list, tag=Microsoft, force-policy=Global Traffic 33 | https://raw.githubusercontent.com/GeQ1an/Rules/master/QuantumultX/Filter/GMedia.list, tag=GMedia, force-policy=Global Traffic 34 | https://raw.githubusercontent.com/GeQ1an/Rules/master/QuantumultX/Filter/PayPal.list, tag=PayPal, force-policy=Global Traffic 35 | https://raw.githubusercontent.com/GeQ1an/Rules/master/QuantumultX/Filter/Speedtest.list, tag=Speedtest, force-policy=Global Traffic 36 | https://raw.githubusercontent.com/GeQ1an/Rules/master/QuantumultX/Filter/Telegram.list, tag=Telegram, force-policy=Global Traffic 37 | https://raw.githubusercontent.com/GeQ1an/Rules/master/QuantumultX/Filter/Outside.list, tag=Outside, force-policy=Global Traffic 38 | https://raw.githubusercontent.com/GeQ1an/Rules/master/QuantumultX/Filter/CMedia.list, tag=CMedia, force-policy=CN Traffic 39 | https://raw.githubusercontent.com/GeQ1an/Rules/master/QuantumultX/Filter/Apple.list, tag=Apple, force-policy=CN Traffic 40 | https://raw.githubusercontent.com/GeQ1an/Rules/master/QuantumultX/Filter/Mainland.list, tag=Mainland, force-policy=CN Traffic 41 | https://raw.githubusercontent.com/GeQ1an/Rules/master/QuantumultX/Filter/Netease%20Music.list, tag=Netease Music, force-policy=CN Traffic 42 | 43 | [filter_local] 44 | host-suffix, local, direct 45 | ip-cidr, 10.0.0.0/8, direct 46 | ip-cidr, 17.0.0.0/8, direct 47 | ip-cidr, 100.64.0.0/10, direct 48 | ip-cidr, 127.0.0.0/8, direct 49 | ip-cidr, 172.16.0.0/12, direct 50 | ip-cidr, 192.168.0.0/16, direct 51 | geoip, cn, CN Traffic 52 | final, Default 53 | 54 | [rewrite_remote] 55 | https://raw.githubusercontent.com/ConnersHua/Profiles/master/Quantumult/X/Rewrite.conf 56 | http://cloudcompute.lbyczf.com/quanx-rewrite 57 | "; 58 | } 59 | 60 | } -------------------------------------------------------------------------------- /ProxyStationService.Tests/ServerFilter/NameFilterTests.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using Microsoft.Extensions.Logging; 3 | using ProxyStation.Model; 4 | using ProxyStation.ServerFilter; 5 | using Xunit; 6 | using Xunit.Abstractions; 7 | using YamlDotNet.RepresentationModel; 8 | 9 | namespace ProxyStation.Tests.ServerFilter 10 | { 11 | public class NameFilterTests 12 | { 13 | NameFilter filter; 14 | 15 | Server[] servers; 16 | 17 | ILogger logger; 18 | 19 | public NameFilterTests(ITestOutputHelper output) 20 | { 21 | this.logger = output.BuildLogger(); 22 | this.filter = new NameFilter(); 23 | this.servers = new Server[]{ 24 | new ShadowsocksServer() { Name = "goodserver 1" }, 25 | new ShadowsocksServer() { Name = "goodserver 2" }, 26 | new ShadowsocksServer() { Name = "goodserver 3" }, 27 | new ShadowsocksServer() { Name = "badserver 1" }, 28 | new ShadowsocksServer() { Name = "badserver 2" }, 29 | }; 30 | } 31 | 32 | [Fact] 33 | public void ShouldSuccessWithWhitelistMode() 34 | { 35 | var optionsYaml = @" 36 | name: name 37 | mode: whitelist 38 | keyword: goodserver 39 | matching: prefix 40 | "; 41 | var yaml = new YamlStream(); 42 | yaml.Load(new StringReader(optionsYaml)); 43 | 44 | filter.LoadOptions(yaml.Documents[0].RootNode, this.logger); 45 | Assert.Equal(FilterMode.WhiteList, filter.Mode); 46 | Assert.Equal(NameFilterMatching.HasPrefix, filter.Matching); 47 | Assert.Equal("goodserver", filter.Keyword); 48 | 49 | var filtered = filter.Do(servers, this.logger); 50 | Assert.Equal(3, filtered.Length); 51 | } 52 | 53 | [Fact] 54 | public void ShouldSuccessWithBlacklistMode() 55 | { 56 | var optionsYaml = @" 57 | name: name 58 | mode: blacklist 59 | keyword: badserver 60 | matching: prefix 61 | "; 62 | var yaml = new YamlStream(); 63 | yaml.Load(new StringReader(optionsYaml)); 64 | 65 | filter.LoadOptions(yaml.Documents[0].RootNode, this.logger); 66 | Assert.Equal(FilterMode.BlackList, filter.Mode); 67 | Assert.Equal(NameFilterMatching.HasPrefix, filter.Matching); 68 | Assert.Equal("badserver", filter.Keyword); 69 | 70 | var filtered = filter.Do(servers, this.logger); 71 | Assert.Equal(3, filtered.Length); 72 | } 73 | 74 | [Fact] 75 | public void ShouldSuccessWithContainsMode() 76 | { 77 | var optionsYaml = @" 78 | name: name 79 | mode: whitelist 80 | keyword: 'server ' 81 | matching: contains 82 | "; 83 | var yaml = new YamlStream(); 84 | yaml.Load(new StringReader(optionsYaml)); 85 | 86 | filter.LoadOptions(yaml.Documents[0].RootNode, this.logger); 87 | Assert.Equal(FilterMode.WhiteList, filter.Mode); 88 | Assert.Equal(NameFilterMatching.Contains, filter.Matching); 89 | Assert.Equal("server ", filter.Keyword); 90 | 91 | var filtered = filter.Do(servers, this.logger); 92 | Assert.Equal(5, filtered.Length); 93 | } 94 | } 95 | } -------------------------------------------------------------------------------- /ProxyStationService.Tests/Parser/SurgeParserTests.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using ProxyStation.Model; 3 | using ProxyStation.ProfileParser; 4 | using Xunit; 5 | using Xunit.Abstractions; 6 | 7 | namespace ProxyStation.Tests.Parser 8 | { 9 | public class SurgeParserTests 10 | { 11 | readonly SurgeParser parser; 12 | 13 | public SurgeParserTests(ITestOutputHelper helper) 14 | { 15 | parser = new SurgeParser(helper.BuildLogger()); 16 | } 17 | 18 | private string GetFixturePath(string relativePath) => Path.Combine("../../..", "./Parser/Fixtures", relativePath); 19 | 20 | [Fact] 21 | public void ShouldSuccess() 22 | { 23 | var servers = parser.Parse(File.ReadAllText(GetFixturePath("Surge.conf"))); 24 | 25 | Assert.IsType(servers[0]); 26 | Assert.Equal("12381293", servers[0].Host); 27 | Assert.Equal(123, servers[0].Port); 28 | Assert.Equal("1231341", ((ShadowsocksServer)servers[0]).Password); 29 | Assert.Equal("sadfasd", servers[0].Name); 30 | Assert.Equal("aes-128-gcm", ((ShadowsocksServer)servers[0]).Method); 31 | Assert.IsType(((ShadowsocksServer)servers[0]).PluginOptions); 32 | Assert.Equal(SimpleObfsPluginMode.HTTP, (((ShadowsocksServer)servers[0]).PluginOptions as SimpleObfsPluginOptions).Mode); 33 | Assert.Equal("2341324124", (((ShadowsocksServer)servers[0]).PluginOptions as SimpleObfsPluginOptions).Host); 34 | 35 | Assert.IsType(servers[1]); 36 | Assert.Equal("hk5.edge.iplc.app", servers[1].Host); 37 | Assert.Equal(155, servers[1].Port); 38 | Assert.Equal("asdads", ((ShadowsocksServer)servers[1]).Password); 39 | Assert.Equal("rc4-md5", ((ShadowsocksServer)servers[1]).Method); 40 | Assert.True(((ShadowsocksServer)servers[1]).UDPRelay); 41 | 42 | Assert.IsType(servers[2]); 43 | Assert.Equal("123.123.123.123", servers[2].Host); 44 | Assert.Equal(10086, servers[2].Port); 45 | Assert.Equal("gasdas", ((ShadowsocksServer)servers[2]).Password); 46 | Assert.Equal("🇭🇰 中国杭州 -> 香港 01 | IPLC", servers[2].Name); 47 | Assert.Equal("xchacha20-ietf-poly1305", ((ShadowsocksServer)servers[2]).Method); 48 | Assert.IsType(((ShadowsocksServer)servers[2]).PluginOptions); 49 | Assert.Equal(SimpleObfsPluginMode.TLS, (((ShadowsocksServer)servers[2]).PluginOptions as SimpleObfsPluginOptions).Mode); 50 | Assert.Equal("download.windowsupdate.com", (((ShadowsocksServer)servers[2]).PluginOptions as SimpleObfsPluginOptions).Host); 51 | Assert.True(((ShadowsocksServer)servers[2]).UDPRelay); 52 | 53 | Assert.IsType(servers[3]); 54 | Assert.Equal("12381293", servers[3].Host); 55 | Assert.Equal(123, servers[3].Port); 56 | Assert.Equal("1231341", ((ShadowsocksServer)servers[3]).Password); 57 | Assert.Equal("sadfasd", servers[3].Name); 58 | Assert.Equal("aes-128-gcm", ((ShadowsocksServer)servers[3]).Method); 59 | Assert.IsType(((ShadowsocksServer)servers[3]).PluginOptions); 60 | Assert.Equal(SimpleObfsPluginMode.HTTP, (((ShadowsocksServer)servers[3]).PluginOptions as SimpleObfsPluginOptions).Mode); 61 | Assert.Equal("2341324124", (((ShadowsocksServer)servers[3]).PluginOptions as SimpleObfsPluginOptions).Host); 62 | } 63 | } 64 | } -------------------------------------------------------------------------------- /ProxyStationService.Tests/Parser/GeneralParserTests.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using Microsoft.Extensions.Logging; 3 | using ProxyStation.Model; 4 | using ProxyStation.ProfileParser; 5 | using Xunit; 6 | 7 | namespace ProxyStation.Tests.Parser 8 | { 9 | public class GeneralParserTests 10 | { 11 | GeneralParser parser; 12 | 13 | public GeneralParserTests() 14 | { 15 | parser = new GeneralParser(new LoggerFactory().CreateLogger("test")); 16 | } 17 | 18 | private string GetFixturePath(string relativePath) => Path.Combine("../../..", "./Parser/Fixtures", relativePath); 19 | 20 | [Fact] 21 | public void ShouldSuccess() 22 | { 23 | var servers = parser.Parse(File.ReadAllText(GetFixturePath("General.conf"))); 24 | 25 | Assert.IsType(servers[0]); 26 | Assert.Equal("127.0.0.1", servers[0].Host); 27 | Assert.Equal(1234, servers[0].Port); 28 | Assert.Equal("aaabbb", ((ShadowsocksRServer)servers[0]).Password); 29 | Assert.Equal("测试中文", servers[0].Name); 30 | Assert.Equal("aes-128-cfb", ((ShadowsocksRServer)servers[0]).Method); 31 | Assert.Equal("auth_aes128_md5", ((ShadowsocksRServer)servers[0]).Protocol); 32 | Assert.Null(((ShadowsocksRServer)servers[0]).ProtocolParameter); 33 | Assert.Equal("tls1.2_ticket_auth", ((ShadowsocksRServer)servers[0]).Obfuscation); 34 | Assert.Equal("breakwa11.moe", ((ShadowsocksRServer)servers[0]).ObfuscationParameter); 35 | Assert.False(((ShadowsocksRServer)servers[0]).UDPOverTCP); 36 | Assert.Equal(0, ((ShadowsocksRServer)servers[0]).UDPPort); 37 | 38 | Assert.IsType(servers[1]); 39 | Assert.Equal("127.0.0.1", servers[1].Host); 40 | Assert.Equal(1234, servers[1].Port); 41 | Assert.Equal("aaabbb", ((ShadowsocksRServer)servers[1]).Password); 42 | Assert.Equal("aes-128-cfb", ((ShadowsocksRServer)servers[1]).Method); 43 | Assert.Equal("auth_aes128_md5", ((ShadowsocksRServer)servers[1]).Protocol); 44 | Assert.Null(((ShadowsocksRServer)servers[1]).ProtocolParameter); 45 | Assert.Equal("tls1.2_ticket_auth", ((ShadowsocksRServer)servers[1]).Obfuscation); 46 | Assert.Equal("breakwa11.moe", ((ShadowsocksRServer)servers[1]).ObfuscationParameter); 47 | Assert.False(((ShadowsocksRServer)servers[1]).UDPOverTCP); 48 | Assert.Equal(0, ((ShadowsocksRServer)servers[1]).UDPPort); 49 | 50 | Assert.IsType(servers[2]); 51 | Assert.Equal("192.168.100.1", servers[2].Host); 52 | Assert.Equal(8888, servers[2].Port); 53 | Assert.Equal("test", ((ShadowsocksServer)servers[2]).Password); 54 | Assert.Equal("Example1", servers[2].Name); 55 | Assert.Equal("aes-128-gcm", ((ShadowsocksServer)servers[2]).Method); 56 | 57 | Assert.IsType(servers[3]); 58 | Assert.Equal("192.168.100.1", servers[3].Host); 59 | Assert.Equal(8888, servers[3].Port); 60 | Assert.Equal("passwd", ((ShadowsocksServer)servers[3]).Password); 61 | Assert.Equal("Example2", servers[3].Name); 62 | Assert.Equal("rc4-md5", ((ShadowsocksServer)servers[3]).Method); 63 | Assert.IsType(((ShadowsocksServer)servers[3]).PluginOptions); 64 | Assert.Equal(SimpleObfsPluginMode.HTTP, ((SimpleObfsPluginOptions)((ShadowsocksServer)servers[3]).PluginOptions).Mode); 65 | 66 | Assert.IsType(servers[4]); 67 | Assert.Equal("192.168.100.1", servers[4].Host); 68 | Assert.Equal(8888, servers[4].Port); 69 | Assert.Equal("test", ((ShadowsocksServer)servers[4]).Password); 70 | Assert.Equal("Dummy profile name", servers[4].Name); 71 | Assert.Equal("bf-cfb", ((ShadowsocksServer)servers[4]).Method); 72 | } 73 | } 74 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ProxyStation 2 | ![Build Stauts](https://github.com/THaGKI9/ProxyStation/workflows/CI%20Test/badge.svg) 3 | [![codecov](https://codecov.io/gh/THaGKI9/ProxyStation/branch/master/graph/badge.svg)](https://codecov.io/gh/THaGKI9/ProxyStation) 4 | 5 | A proxy servers list management program runs on Azure Functions. Support most of shadowsocks client including Surge, Clash, Shadowrocket. 6 | 7 | ### Demo 8 | * General Profile: 9 | * Surge Profile: 10 | * Surge List: 11 | * Clash Profile: 12 | * QuantumultX Profile: 13 | * QuantumultX List: 14 | 15 | ### App Supported 16 | | Alias | App | As source | As output | 17 | |:--------------------|:-----------------------------------------------:|:---------:|:---------:| 18 | | `general` | Shadowrocket
Shadowsocks(R)
Quantumult(X) | √ | √ | 19 | | `surge` | Surge | √ | √ | 20 | | `surge-list` | Surge(Proxy List) | √ | √ | 21 | | `clash` | Clash | √ | √ | 22 | | `quantumult-x` | QuantumultX | √ | √ | 23 | | `quantumult-x-list` | QuantumultX(Proxy List) | √ | √ | 24 | 25 | ### How to Deploy to Azure Functions 26 | Coming soon. 27 | 28 | ### How to Add Profile 29 | This function loads profile from environment variables. 30 | In production, the variables can be set in *Azure Portal*. 31 | During local testing, the variables can be write into `local.settings.json`. 32 | The key name of the variable is profile name, the value is a structured JSON string. 33 | ```jsonc 34 | // Please write in one line. This snippet is just an example 35 | { 36 | "name": "", 37 | "source": "", 38 | "type": "", 39 | 40 | "allowDirectAccess:": "", 41 | "filters": [ // filters are diabled when allowDirectAccess is enabled 42 | { 43 | "name": "", 44 | "mode": "", 45 | ... // filter options 46 | }, 47 | ... 48 | ] 49 | } 50 | 51 | // There is one special case that allows you refer to another profile 52 | { 53 | "name": "", 54 | "source": "", 55 | "type": "alias" 56 | } 57 | ``` 58 | 59 | ### API 60 | 61 | #### Get Built-in Profile 62 | `GET /api/train/{profile-name}/{output?}?template={templateUrlOrName}` 63 | 64 | Description: 65 | Developer can pre-define some profile sources before deploy to *Azure Functions*. 66 | Invoking this API will cause function to retrive profile from source accordingly, 67 | parse the source and format to specific format according to argument `output`. 68 | 69 | Query: 70 | * `templateUrlOrName` is a url to custom template or pre-defined name in environment variables table. 71 | 72 | Argument: 73 | * `profile-name` is pre-defined name of profile source 74 | * `output` is the type of target profile, if omitted, will be inferred from user-agent. If `output = original` and `allowDirectAccess` is enabled for this profile, service will return original content downloading from source url. 75 | 76 | 77 | ### Supported Filters 78 | Filters allow you to control outcome of servers list. 79 | 80 | #### Server Name Filter 81 | ```jsonc 82 | { 83 | "name": "name", 84 | "keyword": "", 85 | "matching": "" 86 | } 87 | ``` 88 | 89 | ### Server Name Regular Expression Filter 90 | ```jsonc 91 | { 92 | "name": "regex", 93 | "pattern": "", 94 | } 95 | ``` -------------------------------------------------------------------------------- /ProxyStationService.Tests/Parser/ClashParserTests.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using Microsoft.Extensions.Logging; 3 | using ProxyStation.Model; 4 | using ProxyStation.ProfileParser; 5 | using Xunit; 6 | using Xunit.Abstractions; 7 | 8 | namespace ProxyStation.Tests.Parser 9 | { 10 | public class ClashParserTests 11 | { 12 | readonly ClashParser parser; 13 | 14 | public ClashParserTests(ITestOutputHelper output) 15 | { 16 | parser = new ClashParser(output.BuildLogger()); 17 | } 18 | 19 | private string GetFixturePath(string relativePath) => Path.Combine("../../..", "./Parser/Fixtures", relativePath); 20 | 21 | [Fact] 22 | public void ShouldSuccess() 23 | { 24 | var servers = parser.Parse(File.ReadAllText(GetFixturePath("Clash.yaml"))); 25 | 26 | Assert.Equal(4, servers.Length); 27 | 28 | Assert.IsType(servers[0]); 29 | Assert.Equal("gg.gol.proxy", servers[0].Host); 30 | Assert.Equal(152, servers[0].Port); 31 | Assert.Equal("asdbasv", ((ShadowsocksServer)servers[0]).Password); 32 | Assert.Equal("🇺🇸 中国上海 -> 美国 01 | IPLC", servers[0].Name); 33 | Assert.Equal("chacha20-ietf-poly1305", ((ShadowsocksServer)servers[0]).Method); 34 | Assert.IsType(((ShadowsocksServer)servers[0]).PluginOptions); 35 | Assert.Equal(SimpleObfsPluginMode.TLS, (((ShadowsocksServer)servers[0]).PluginOptions as SimpleObfsPluginOptions).Mode); 36 | Assert.Equal("adfadfads", (((ShadowsocksServer)servers[0]).PluginOptions as SimpleObfsPluginOptions).Host); 37 | Assert.True(((ShadowsocksServer)servers[0]).UDPRelay); 38 | 39 | Assert.IsType(servers[1]); 40 | Assert.Equal("gg.gol.proxy", servers[1].Host); 41 | Assert.Equal(152, servers[1].Port); 42 | Assert.Equal("asdbasv", ((ShadowsocksServer)servers[1]).Password); 43 | Assert.Equal("🇺🇸 中国上海 -> 美国 01 | IPLC", servers[1].Name); 44 | Assert.Equal("chacha20-ietf-poly1305", ((ShadowsocksServer)servers[1]).Method); 45 | Assert.IsType(((ShadowsocksServer)servers[1]).PluginOptions); 46 | Assert.Equal(SimpleObfsPluginMode.TLS, (((ShadowsocksServer)servers[1]).PluginOptions as SimpleObfsPluginOptions).Mode); 47 | Assert.Equal("asdfasdf", (((ShadowsocksServer)servers[1]).PluginOptions as SimpleObfsPluginOptions).Host); 48 | Assert.False(((ShadowsocksServer)servers[1]).UDPRelay); 49 | 50 | Assert.IsType(servers[2]); 51 | Assert.Equal("server", servers[2].Host); 52 | Assert.Equal(443, servers[2].Port); 53 | Assert.Equal("password", ((ShadowsocksServer)servers[2]).Password); 54 | Assert.Equal("ss3", servers[2].Name); 55 | Assert.Equal("AEAD_CHACHA20_POLY1305", ((ShadowsocksServer)servers[2]).Method); 56 | Assert.IsType(((ShadowsocksServer)servers[2]).PluginOptions); 57 | Assert.Equal(V2RayPluginMode.WebSocket, (((ShadowsocksServer)servers[2]).PluginOptions as V2RayPluginOptions).Mode); 58 | Assert.Equal("bing.com", (((ShadowsocksServer)servers[2]).PluginOptions as V2RayPluginOptions).Host); 59 | Assert.Equal("/", (((ShadowsocksServer)servers[2]).PluginOptions as V2RayPluginOptions).Path); 60 | Assert.True((((ShadowsocksServer)servers[2]).PluginOptions as V2RayPluginOptions).SkipCertVerification); 61 | Assert.True((((ShadowsocksServer)servers[2]).PluginOptions as V2RayPluginOptions).EnableTLS); 62 | Assert.Equal(2, (((ShadowsocksServer)servers[2]).PluginOptions as V2RayPluginOptions).Headers.Count); 63 | Assert.Equal("value", (((ShadowsocksServer)servers[2]).PluginOptions as V2RayPluginOptions).Headers["custom"]); 64 | Assert.Equal("bar", (((ShadowsocksServer)servers[2]).PluginOptions as V2RayPluginOptions).Headers["foo"]); 65 | Assert.False(((ShadowsocksServer)servers[2]).UDPRelay); 66 | 67 | Assert.IsType(servers[3]); 68 | Assert.Equal("server", servers[3].Host); 69 | Assert.Equal(443, servers[3].Port); 70 | Assert.Equal("password", ((ShadowsocksServer)servers[3]).Password); 71 | Assert.Equal("ss5", servers[3].Name); 72 | Assert.Equal("AEAD_CHACHA20_POLY1305", ((ShadowsocksServer)servers[3]).Method); 73 | Assert.Null(((ShadowsocksServer)servers[3]).PluginOptions); 74 | } 75 | } 76 | } -------------------------------------------------------------------------------- /ProxyStationService/Util/Misc.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace ProxyStation.Util 6 | { 7 | public class Misc 8 | { 9 | public static string KebabCase2PascalCase(string kebabCase) 10 | { 11 | var words = kebabCase 12 | .Split('-') 13 | .Select(w => w.Substring(0, 1).ToUpper() + w.Substring(1)) 14 | .Aggregate((a, b) => a + b); 15 | return words; 16 | } 17 | 18 | public static bool SplitHostAndPort(string hostPort, out string host, out int port) 19 | { 20 | host = default; 21 | port = default; 22 | 23 | if (string.IsNullOrEmpty(hostPort)) return false; 24 | 25 | var parts = hostPort.Split(":"); 26 | if (parts.Length != 2) return false; 27 | 28 | if (!int.TryParse(parts[1], out int _port)) return false; 29 | if (_port > 0xFFFF || _port < 1) return false; 30 | 31 | host = parts[0]; 32 | port = _port; 33 | return true; 34 | } 35 | 36 | /// 37 | /// Parse properties file like: 38 | /// [Section1] 39 | /// key=value 40 | /// key=value 41 | /// 42 | /// [Section2] 43 | /// key=value 44 | /// key=value 45 | /// into 46 | /// 47 | /// Content of a properties file 48 | /// a dictionary whose key is section name and value is a list of text lines in this section 49 | public static Dictionary> ParsePropertieFile(string fileContent, string commentLinePrefixes = "#;") 50 | { 51 | var result = new Dictionary>(); 52 | var commentPrefix = commentLinePrefixes 53 | .Select(c => c) 54 | .Where(c => c != ' ') 55 | .ToHashSet(); 56 | var pos = 0; 57 | var length = fileContent.Length; 58 | 59 | // skip whitespaces in the beginning 60 | while (pos < length && char.IsWhiteSpace(fileContent[pos])) pos++; 61 | 62 | var sectionName = default(string); 63 | var sectionValues = default(List); 64 | 65 | for (; pos < length;) 66 | { 67 | // move to next non-whitespace char or EOF 68 | if (char.IsWhiteSpace(fileContent[pos])) 69 | { 70 | pos++; 71 | continue; 72 | } 73 | 74 | var c = fileContent[pos]; 75 | if (c == '[') 76 | { 77 | // capture section name 78 | var startPos = pos; 79 | for (; pos < length && fileContent[pos] != ']'; pos++) 80 | { 81 | if (fileContent[pos] == '\n' || fileContent[pos] == '\r') 82 | { 83 | // mutli-line section name is illegal 84 | throw new FormatException($"Expect ] but get EOL. Pos: {pos}"); 85 | } 86 | } 87 | 88 | if (pos >= length) 89 | { 90 | throw new FormatException($"Expect ] but get EOF"); 91 | } 92 | 93 | sectionName = fileContent[(startPos + 1)..pos]; 94 | sectionValues = new List(); 95 | result.TryAdd(sectionName, sectionValues); 96 | pos++; 97 | } 98 | else if (commentPrefix.Contains(c)) 99 | { 100 | // comment line, move to next whitespace or EOF 101 | for (; pos < length && fileContent[pos] != '\r' && fileContent[pos] != '\n'; pos++) ; 102 | } 103 | else if (sectionValues == default) // values are not contained in a section 104 | { 105 | throw new FormatException($"Expect [ or EOF but get character #{Convert.ToUInt32(c)}(ASCII)`. Pos: {pos}"); 106 | } 107 | else 108 | { 109 | // capture section values 110 | var startPos = pos; 111 | for (; pos < length && fileContent[pos] != '\n' && fileContent[pos] != '\r'; pos++) ; 112 | sectionValues.Add(fileContent[startPos..pos]); 113 | } 114 | } 115 | 116 | return result; 117 | } 118 | } 119 | } -------------------------------------------------------------------------------- /ProxyStationService/Util/Downloader.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Linq; 4 | using System.Net; 5 | using System.Security.Cryptography; 6 | using System.Text; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | using Azure.Storage.Blobs; 10 | using Azure.Storage.Blobs.Models; 11 | using Microsoft.Extensions.Logging; 12 | 13 | namespace ProxyStation.Util 14 | { 15 | public class Downloader : IDownloader 16 | { 17 | readonly string azureStorageConnectionString; 18 | 19 | public Downloader() : this(null) 20 | { 21 | } 22 | 23 | public Downloader(string azureStorageConnectionString) 24 | { 25 | this.azureStorageConnectionString = azureStorageConnectionString; 26 | } 27 | 28 | public async Task Download(ILogger logger, string url) 29 | { 30 | try 31 | { 32 | var content = await this.DownloadRaw(logger, url); 33 | if (azureStorageConnectionString != null) 34 | { 35 | try 36 | { 37 | await this.UpdateCache(logger, url, Encoding.UTF8.GetBytes(content)); 38 | logger.LogInformation($"Updated cache for url {url}"); 39 | 40 | } 41 | catch (Exception ex) 42 | { 43 | logger.LogError($"Fail to update cache. Exception: {ex.Message}.\n StackTrace: {ex.StackTrace}"); 44 | } 45 | } 46 | return content; 47 | } 48 | catch (Exception ex) 49 | { 50 | if (this.azureStorageConnectionString != null) 51 | { 52 | logger.LogError($"Fail to download resource. Will try to use cache. Exception: {ex.Message}.\n StackTrace: {ex.StackTrace}"); 53 | var cache = await this.GetCache(logger, url); 54 | 55 | if (cache == null || cache.Length == 0) 56 | { 57 | logger.LogError($"Get null cache for url {url}"); 58 | return null; 59 | } 60 | logger.LogInformation($"Load cache for url {url}"); 61 | return Encoding.UTF8.GetString(cache); 62 | } 63 | else 64 | { 65 | return null; 66 | } 67 | } 68 | } 69 | 70 | public async Task DownloadRaw(ILogger logger, string url) 71 | { 72 | var request = WebRequest.Create(url); 73 | request.Headers.Set("User-Agent", "proxy-station/1.0.0"); 74 | request.Timeout = (int)Constant.DownloadTimeout.TotalMilliseconds; 75 | 76 | using var response = (await request.GetResponseAsync()) as HttpWebResponse; 77 | if (response.StatusCode != HttpStatusCode.OK) 78 | { 79 | throw new Exception($"Fail to download {url}。 Status code: {response.StatusCode}"); 80 | } 81 | using var dataStream = response.GetResponseStream(); 82 | return await new StreamReader(dataStream).ReadToEndAsync(); 83 | } 84 | 85 | CancellationToken GetCacheTimeoutToken() => new CancellationTokenSource(Constant.CacheOperationTimeout).Token; 86 | 87 | string GetCacheKey(string plainText) 88 | { 89 | var hash = new SHA1Managed().ComputeHash(Encoding.UTF8.GetBytes(plainText)); 90 | return string.Concat(hash.Select(b => b.ToString("X2"))); 91 | } 92 | 93 | async Task GetCache(ILogger logger, string url) 94 | { 95 | string connectionString = azureStorageConnectionString; 96 | string containerName = Constant.DownloaderCacheBlobContainer; 97 | string blobName = this.GetCacheKey(url); 98 | 99 | var container = new BlobContainerClient(connectionString, containerName); 100 | var blob = container.GetBlobClient(blobName); 101 | var exist = await blob.ExistsAsync(this.GetCacheTimeoutToken()); 102 | if (!exist) 103 | { 104 | logger.LogDebug($"Cache for url `{url}` is not missing"); 105 | return null; 106 | } 107 | 108 | var response = await blob.DownloadAsync(this.GetCacheTimeoutToken()); 109 | using var memoryStream = new MemoryStream(); 110 | response.Value.Content.CopyTo(memoryStream); 111 | 112 | return memoryStream.ToArray(); 113 | } 114 | 115 | async Task UpdateCache(ILogger logger, string url, byte[] data) 116 | { 117 | string connectionString = azureStorageConnectionString; 118 | string containerName = Constant.DownloaderCacheBlobContainer; 119 | string blobName = this.GetCacheKey(url); 120 | 121 | var container = new BlobContainerClient(connectionString, containerName); 122 | var blob = container.GetBlobClient(blobName); 123 | await blob.DeleteIfExistsAsync(); 124 | await blob.UploadAsync(new MemoryStream(data)); 125 | return true; 126 | } 127 | } 128 | } -------------------------------------------------------------------------------- /ProxyStationService.Tests/Util/MiscTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using ProxyStation.Util; 4 | using Xunit; 5 | 6 | namespace ProxyStation.Tests.Util 7 | { 8 | public class MiscTests 9 | { 10 | #region ParsePropertiesFile 11 | const string SomeWhiteSpaces = " \t \t "; 12 | 13 | [Fact] 14 | public void TestParsePropertiesFile_ShouldSuccess() 15 | { 16 | // Normal Case 17 | { 18 | var properties = string.Join("\r\n", new string[] { 19 | "# CommentLine1", 20 | $"[Section{MiscTests.SomeWhiteSpaces}1]", 21 | $"; CommentLine2{MiscTests.SomeWhiteSpaces}", 22 | "Key1=Value1", 23 | $"Key2=Value2{MiscTests.SomeWhiteSpaces}", 24 | MiscTests.SomeWhiteSpaces, 25 | "", 26 | $"Key3={MiscTests.SomeWhiteSpaces}Value3", 27 | "", 28 | MiscTests.SomeWhiteSpaces, 29 | "", 30 | $"[{MiscTests.SomeWhiteSpaces}Section 2]", 31 | "Key1=Value1", 32 | "", 33 | "", 34 | "[Section 3]", 35 | }); 36 | var result = Misc.ParsePropertieFile(properties); 37 | 38 | Assert.NotNull(result); 39 | Assert.Equal(3, result.Count); 40 | Assert.Contains($"Section{MiscTests.SomeWhiteSpaces}1", (IDictionary>)result); 41 | Assert.Contains($"{MiscTests.SomeWhiteSpaces}Section 2", (IDictionary>)result); 42 | Assert.Contains("Section 3", (IDictionary>)result); 43 | 44 | var section1Value = result[$"Section{MiscTests.SomeWhiteSpaces}1"]; 45 | Assert.NotNull(section1Value); 46 | Assert.Equal(3, section1Value.Count); 47 | Assert.Equal("Key1=Value1", section1Value[0]); 48 | Assert.Equal($"Key2=Value2{MiscTests.SomeWhiteSpaces}", section1Value[1]); 49 | Assert.Equal($"Key3={MiscTests.SomeWhiteSpaces}Value3", section1Value[2]); 50 | 51 | var section2Value = result[$"{MiscTests.SomeWhiteSpaces}Section 2"]; 52 | Assert.NotNull(section2Value); 53 | Assert.Single(section2Value); 54 | Assert.Equal("Key1=Value1", section2Value[0]); 55 | 56 | var section3Value = result["Section 3"]; 57 | Assert.NotNull(section3Value); 58 | Assert.Empty(section3Value); 59 | } 60 | 61 | // Custom comment prefix 62 | { 63 | var properties = string.Join("\r\n", new string[] { 64 | $"[Section 1]", 65 | $"$ CommentLine2 ", 66 | "-Key1=Value1", 67 | "Key2=Value2 ", 68 | "Key3=Value3", 69 | }); 70 | var result = Misc.ParsePropertieFile(properties, "$-"); 71 | 72 | Assert.NotNull(result); 73 | Assert.Single(result); 74 | Assert.Contains("Section 1", (IDictionary>)result); 75 | 76 | 77 | var section1Value = result[$"Section 1"]; 78 | Assert.NotNull(section1Value); 79 | Assert.Equal(2, section1Value.Count); 80 | Assert.Equal("Key2=Value2 ", section1Value[0]); 81 | Assert.Equal("Key3=Value3", section1Value[1]); 82 | } 83 | } 84 | 85 | [Fact] 86 | public void TestParsePropertiesFile_UncloseBracket() 87 | { 88 | // Case: EOL 89 | var ex1 = Assert.Throws(() => 90 | { 91 | var properties = string.Join("\r\n", new string[] { 92 | $"[Section 1", 93 | $"$ CommentLine2 ", 94 | "-Key1=Value1", 95 | "Key2=Value2 ", 96 | "Key3=Value3", 97 | }); 98 | var result = Misc.ParsePropertieFile(properties, "$-"); 99 | }); 100 | Assert.Contains("Expect ] but get EOL", ex1.Message); 101 | 102 | // Case: EOF 103 | var ex2 = Assert.Throws(() => 104 | { 105 | var properties = $"[Section 1"; 106 | var result = Misc.ParsePropertieFile(properties, "$-"); 107 | }); 108 | Assert.Contains("Expect ] but get EOF", ex2.Message); 109 | } 110 | 111 | [Fact] 112 | public void TestParsePropertiesFile_ValuesUnderRoot() 113 | { 114 | // Case: 115 | var ex1 = Assert.Throws(() => 116 | { 117 | var properties = string.Join("\r\n", new string[] { 118 | "-Key1=Value1", 119 | "Key2=Value2 ", 120 | "Key3=Value3", 121 | }); 122 | var result = Misc.ParsePropertieFile(properties, "$-"); 123 | }); 124 | Assert.Contains("Expect [ or EOF but get character", ex1.Message); 125 | } 126 | #endregion ParsePropertiesFile 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | coverage/ 5 | coverage.opencover.xml 6 | coverage.json 7 | 8 | # Azure Functions localsettings file 9 | local.settings.json 10 | 11 | # User-specific files 12 | *.suo 13 | *.user 14 | *.userosscache 15 | *.sln.docstates 16 | 17 | # User-specific files (MonoDevelop/Xamarin Studio) 18 | *.userprefs 19 | 20 | # Build results 21 | [Dd]ebug/ 22 | [Dd]ebugPublic/ 23 | [Rr]elease/ 24 | [Rr]eleases/ 25 | x64/ 26 | x86/ 27 | bld/ 28 | [Bb]in/ 29 | [Oo]bj/ 30 | [Ll]og/ 31 | 32 | # Visual Studio 2015 cache/options directory 33 | .vscode/ 34 | .vs/ 35 | # Uncomment if you have tasks that create the project's static files in wwwroot 36 | #wwwroot/ 37 | 38 | # MSTest test Results 39 | [Tt]est[Rr]esult*/ 40 | [Bb]uild[Ll]og.* 41 | 42 | # NUNIT 43 | *.VisualState.xml 44 | TestResult.xml 45 | 46 | # Build Results of an ATL Project 47 | [Dd]ebugPS/ 48 | [Rr]eleasePS/ 49 | dlldata.c 50 | 51 | # DNX 52 | project.lock.json 53 | project.fragment.lock.json 54 | artifacts/ 55 | 56 | *_i.c 57 | *_p.c 58 | *_i.h 59 | *.ilk 60 | *.meta 61 | *.obj 62 | *.pch 63 | *.pdb 64 | *.pgc 65 | *.pgd 66 | *.rsp 67 | *.sbr 68 | *.tlb 69 | *.tli 70 | *.tlh 71 | *.tmp 72 | *.tmp_proj 73 | *.log 74 | *.vspscc 75 | *.vssscc 76 | .builds 77 | *.pidb 78 | *.svclog 79 | *.scc 80 | 81 | # Chutzpah Test files 82 | _Chutzpah* 83 | 84 | # Visual C++ cache files 85 | ipch/ 86 | *.aps 87 | *.ncb 88 | *.opendb 89 | *.opensdf 90 | *.sdf 91 | *.cachefile 92 | *.VC.db 93 | *.VC.VC.opendb 94 | 95 | # Visual Studio profiler 96 | *.psess 97 | *.vsp 98 | *.vspx 99 | *.sap 100 | 101 | # TFS 2012 Local Workspace 102 | $tf/ 103 | 104 | # Guidance Automation Toolkit 105 | *.gpState 106 | 107 | # ReSharper is a .NET coding add-in 108 | _ReSharper*/ 109 | *.[Rr]e[Ss]harper 110 | *.DotSettings.user 111 | 112 | # JustCode is a .NET coding add-in 113 | .JustCode 114 | 115 | # TeamCity is a build add-in 116 | _TeamCity* 117 | 118 | # DotCover is a Code Coverage Tool 119 | *.dotCover 120 | 121 | # NCrunch 122 | _NCrunch_* 123 | .*crunch*.local.xml 124 | nCrunchTemp_* 125 | 126 | # MightyMoose 127 | *.mm.* 128 | AutoTest.Net/ 129 | 130 | # Web workbench (sass) 131 | .sass-cache/ 132 | 133 | # Installshield output folder 134 | [Ee]xpress/ 135 | 136 | # DocProject is a documentation generator add-in 137 | DocProject/buildhelp/ 138 | DocProject/Help/*.HxT 139 | DocProject/Help/*.HxC 140 | DocProject/Help/*.hhc 141 | DocProject/Help/*.hhk 142 | DocProject/Help/*.hhp 143 | DocProject/Help/Html2 144 | DocProject/Help/html 145 | 146 | # Click-Once directory 147 | publish/ 148 | 149 | # Publish Web Output 150 | *.[Pp]ublish.xml 151 | *.azurePubxml 152 | # TODO: Comment the next line if you want to checkin your web deploy settings 153 | # but database connection strings (with potential passwords) will be unencrypted 154 | #*.pubxml 155 | *.publishproj 156 | 157 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 158 | # checkin your Azure Web App publish settings, but sensitive information contained 159 | # in these scripts will be unencrypted 160 | PublishScripts/ 161 | 162 | # NuGet Packages 163 | *.nupkg 164 | # The packages folder can be ignored because of Package Restore 165 | **/packages/* 166 | # except build/, which is used as an MSBuild target. 167 | !**/packages/build/ 168 | # Uncomment if necessary however generally it will be regenerated when needed 169 | #!**/packages/repositories.config 170 | # NuGet v3's project.json files produces more ignoreable files 171 | *.nuget.props 172 | *.nuget.targets 173 | 174 | # Microsoft Azure Build Output 175 | csx/ 176 | *.build.csdef 177 | 178 | # Microsoft Azure Emulator 179 | ecf/ 180 | rcf/ 181 | 182 | # Windows Store app package directories and files 183 | AppPackages/ 184 | BundleArtifacts/ 185 | Package.StoreAssociation.xml 186 | _pkginfo.txt 187 | 188 | # Visual Studio cache files 189 | # files ending in .cache can be ignored 190 | *.[Cc]ache 191 | # but keep track of directories ending in .cache 192 | !*.[Cc]ache/ 193 | 194 | # Others 195 | ClientBin/ 196 | ~$* 197 | *~ 198 | *.dbmdl 199 | *.dbproj.schemaview 200 | *.jfm 201 | *.pfx 202 | *.publishsettings 203 | node_modules/ 204 | orleans.codegen.cs 205 | 206 | # Since there are multiple workflows, uncomment next line to ignore bower_components 207 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 208 | #bower_components/ 209 | 210 | # RIA/Silverlight projects 211 | Generated_Code/ 212 | 213 | # Backup & report files from converting an old project file 214 | # to a newer Visual Studio version. Backup files are not needed, 215 | # because we have git ;-) 216 | _UpgradeReport_Files/ 217 | Backup*/ 218 | UpgradeLog*.XML 219 | UpgradeLog*.htm 220 | 221 | # SQL Server files 222 | *.mdf 223 | *.ldf 224 | 225 | # Business Intelligence projects 226 | *.rdl.data 227 | *.bim.layout 228 | *.bim_*.settings 229 | 230 | # Microsoft Fakes 231 | FakesAssemblies/ 232 | 233 | # GhostDoc plugin setting file 234 | *.GhostDoc.xml 235 | 236 | # Node.js Tools for Visual Studio 237 | .ntvs_analysis.dat 238 | 239 | # Visual Studio 6 build log 240 | *.plg 241 | 242 | # Visual Studio 6 workspace options file 243 | *.opt 244 | 245 | # Visual Studio LightSwitch build output 246 | **/*.HTMLClient/GeneratedArtifacts 247 | **/*.DesktopClient/GeneratedArtifacts 248 | **/*.DesktopClient/ModelManifest.xml 249 | **/*.Server/GeneratedArtifacts 250 | **/*.Server/ModelManifest.xml 251 | _Pvt_Extensions 252 | 253 | # Paket dependency manager 254 | .paket/paket.exe 255 | paket-files/ 256 | 257 | # FAKE - F# Make 258 | .fake/ 259 | 260 | # JetBrains Rider 261 | .idea/ 262 | *.sln.iml 263 | 264 | # CodeRush 265 | .cr/ 266 | 267 | # Python Tools for Visual Studio (PTVS) 268 | __pycache__/ 269 | *.pyc -------------------------------------------------------------------------------- /ProxyStationService.Tests/Parser/Fixtures/Demo.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | port: 7890 3 | socks-port: 7891 4 | allow-lan: true 5 | mode: Rule 6 | log-level: silent 7 | external-controller: 0.0.0.0:9090 8 | dns: 9 | enable: true 10 | ipv6: false 11 | listen: 0.0.0.0:53 12 | enhanced-mode: redir-host 13 | nameserver: 14 | - 117.50.10.10 15 | - 119.29.29.29 16 | - 223.5.5.5 17 | - tls://dns.rubyfish.cn:853 18 | fallback: 19 | - tls://1.1.1.1:853 20 | - tls://1.0.0.1:853 21 | - tls://dns.google:853 22 | Proxy: 23 | - name: "Test Proxy Server 1" 24 | type: ss 25 | server: gg.gol.proxy 26 | port: 152 27 | cipher: chacha20-ietf-poly1305 28 | password: asdbasv 29 | udp: true 30 | obfs: tls 31 | obfs-host: adfadfads 32 | - name: "Test Proxy Server 2" 33 | type: ss 34 | server: gg.gol.proxy 35 | port: 152 36 | cipher: chacha20-ietf-poly1305 37 | password: asdbasv 38 | plugin: obfs 39 | plugin-opts: 40 | mode: tls 41 | host: asdfasdf 42 | - name: "Test Proxy Server 3" 43 | type: ss 44 | server: server 45 | port: 443 46 | cipher: AEAD_CHACHA20_POLY1305 47 | password: "password" 48 | plugin: v2ray-plugin 49 | plugin-opts: 50 | mode: websocket 51 | tls: true 52 | skip-cert-verify: true 53 | host: bing.com 54 | path: "/" 55 | headers: 56 | custom: value 57 | foo: bar 58 | - name: "Test Proxy Server 4" 59 | type: ss 60 | server: server 61 | port: 443 62 | cipher: AEAD_CHACHA20_POLY1305 63 | password: "password" 64 | Rule: 65 | - DOMAIN-SUFFIX,edgedatg.com,GlobalTV 66 | - DOMAIN-SUFFIX,go.com,GlobalTV 67 | - DOMAIN,linear-abematv.akamaized.net,GlobalTV 68 | - DOMAIN-SUFFIX,abema.io,GlobalTV 69 | - DOMAIN-SUFFIX,abema.tv,GlobalTV 70 | - DOMAIN-SUFFIX,akamaized.net,GlobalTV 71 | - DOMAIN-SUFFIX,ameba.jp,GlobalTV 72 | - DOMAIN-SUFFIX,hayabusa.io,GlobalTV 73 | - DOMAIN-SUFFIX,aiv-cdn.net,GlobalTV 74 | - DOMAIN-SUFFIX,amazonaws.com,GlobalTV 75 | - DOMAIN-SUFFIX,amazonvideo.com,GlobalTV 76 | - DOMAIN-SUFFIX,llnwd.net,GlobalTV 77 | - DOMAIN-SUFFIX,bahamut.com.tw,GlobalTV 78 | - DOMAIN-SUFFIX,gamer.com.tw,GlobalTV 79 | - DOMAIN-SUFFIX,hinet.net,GlobalTV 80 | - DOMAIN-KEYWORD,bbcfmt,GlobalTV 81 | - DOMAIN-KEYWORD,co.uk,GlobalTV 82 | - DOMAIN-KEYWORD,uk-live,GlobalTV 83 | - DOMAIN-SUFFIX,bbc.co,GlobalTV 84 | - DOMAIN-SUFFIX,bbc.co.uk,GlobalTV 85 | - DOMAIN-SUFFIX,bbc.com,GlobalTV 86 | - DOMAIN-SUFFIX,bbci.co,GlobalTV 87 | - DOMAIN-SUFFIX,bbci.co.uk,GlobalTV 88 | - DOMAIN-SUFFIX,chocotv.com.tw,GlobalTV 89 | - DOMAIN-KEYWORD,epicgames,GlobalTV 90 | - DOMAIN-SUFFIX,helpshift.com,GlobalTV 91 | - DOMAIN-KEYWORD,foxplus,GlobalTV 92 | - DOMAIN-SUFFIX,config.fox.com,GlobalTV 93 | - DOMAIN-SUFFIX,emome.net,GlobalTV 94 | - DOMAIN-SUFFIX,fox.com,GlobalTV 95 | - DOMAIN-SUFFIX,foxdcg.com,GlobalTV 96 | - DOMAIN-SUFFIX,foxnow.com,GlobalTV 97 | - DOMAIN-SUFFIX,foxplus.com,GlobalTV 98 | - DOMAIN-SUFFIX,foxplay.com,GlobalTV 99 | - DOMAIN-SUFFIX,ipinfo.io,GlobalTV 100 | - DOMAIN-SUFFIX,mstage.io,GlobalTV 101 | - DOMAIN-SUFFIX,now.com,GlobalTV 102 | - DOMAIN-SUFFIX,theplatform.com,GlobalTV 103 | - DOMAIN-SUFFIX,urlload.net,GlobalTV 104 | - DOMAIN-SUFFIX,execute-api.ap-southeast-1.amazonaws.com,GlobalTV 105 | - DOMAIN-SUFFIX,hbo.com,GlobalTV 106 | - DOMAIN-SUFFIX,hboasia.com,GlobalTV 107 | - DOMAIN-SUFFIX,hbogo.com,GlobalTV 108 | - DOMAIN-SUFFIX,hbogoasia.hk,GlobalTV 109 | - DOMAIN-SUFFIX,happyon.jp,GlobalTV 110 | - DOMAIN-SUFFIX,hulu.com,GlobalTV 111 | - DOMAIN-SUFFIX,huluim.com,GlobalTV 112 | - DOMAIN-SUFFIX,hulustream.com,GlobalTV 113 | - DOMAIN-SUFFIX,imkan.tv,GlobalTV 114 | - DOMAIN-SUFFIX,joox.com,GlobalTV 115 | - DOMAIN-KEYWORD,nowtv100,GlobalTV 116 | - DOMAIN-KEYWORD,rthklive,GlobalTV 117 | - DOMAIN-SUFFIX,mytvsuper.com,GlobalTV 118 | - DOMAIN-SUFFIX,tvb.com,GlobalTV 119 | - DOMAIN-SUFFIX,netflix.com,GlobalTV 120 | - DOMAIN-SUFFIX,netflix.net,GlobalTV 121 | - DOMAIN-SUFFIX,nflxext.com,GlobalTV 122 | - DOMAIN-SUFFIX,nflximg.com,GlobalTV 123 | - DOMAIN-SUFFIX,nflximg.net,GlobalTV 124 | - DOMAIN-SUFFIX,nflxso.net,GlobalTV 125 | - DOMAIN-SUFFIX,nflxvideo.net,GlobalTV 126 | - DOMAIN-SUFFIX,pandora.com,GlobalTV 127 | - DOMAIN-SUFFIX,sky.com,GlobalTV 128 | - DOMAIN-SUFFIX,skygo.co.nz,GlobalTV 129 | - DOMAIN-KEYWORD,spotify,GlobalTV 130 | - DOMAIN-SUFFIX,scdn.co,GlobalTV 131 | - DOMAIN-SUFFIX,spoti.fi,GlobalTV 132 | - DOMAIN-SUFFIX,viu.tv,GlobalTV 133 | - DOMAIN-KEYWORD,youtube,GlobalTV 134 | - DOMAIN-SUFFIX,googlevideo.com,GlobalTV 135 | - DOMAIN-SUFFIX,gvt2.com,GlobalTV 136 | - DOMAIN-SUFFIX,youtu.be,GlobalTV 137 | - DOMAIN-KEYWORD,bilibili,AsianTV 138 | - DOMAIN-SUFFIX,acg.tv,AsianTV 139 | - DOMAIN-SUFFIX,acgvideo.com,AsianTV 140 | - DOMAIN-SUFFIX,b23.tv,AsianTV 141 | - DOMAIN-SUFFIX,biliapi.com,AsianTV 142 | - DOMAIN-SUFFIX,biliapi.net,AsianTV 143 | - DOMAIN-SUFFIX,bilibili.com,AsianTV 144 | - DOMAIN-SUFFIX,biligame.com,AsianTV 145 | - DOMAIN-SUFFIX,biligame.net,AsianTV 146 | - DOMAIN-SUFFIX,hdslb.com,AsianTV 147 | - DOMAIN-SUFFIX,im9.com,AsianTV 148 | - DOMAIN-KEYWORD,qiyi,AsianTV 149 | - DOMAIN-SUFFIX,qy.net,AsianTV 150 | - DOMAIN-SUFFIX,api.mob.app.letv.com,AsianTV 151 | - DOMAIN-SUFFIX,163yun.com,AsianTV 152 | - DOMAIN-SUFFIX,music.126.net,AsianTV 153 | - DOMAIN-SUFFIX,music.163.com,AsianTV 154 | - DOMAIN-SUFFIX,vv.video.qq.com,AsianTV 155 | - DOMAIN,analytics.google.com,Proxy 156 | - DOMAIN,analyticsinsights-pa.googleapis.com,Proxy 157 | - DOMAIN,analyticsreporting.googleapis.com,Proxy 158 | - DOMAIN-SUFFIX,vd.l.qq.com,Domestic 159 | - DOMAIN-KEYWORD,adservice,AdBlock 160 | - DOMAIN-KEYWORD,analytics,AdBlock 161 | - DOMAIN-KEYWORD,analysis,AdBlock 162 | - DOMAIN-SUFFIX,3lift.com,AdBlock 163 | - DOMAIN-SUFFIX,4006825178.com,AdBlock 164 | - DOMAIN-SUFFIX,51.la,AdBlock 165 | - DOMAIN-SUFFIX,550tg.com,AdBlock 166 | - DOMAIN-SUFFIX,56txs4.com,AdBlock 167 | - DOMAIN-SUFFIX,ad373.com,AdBlock 168 | - DOMAIN-SUFFIX,ad4screen.com,AdBlock 169 | - DOMAIN-SUFFIX,ad-brix.com,AdBlock 170 | - DOMAIN-SUFFIX,adcolony.com,AdBlock 171 | - DOMAIN-SUFFIX,adform.net,AdBlock 172 | - DOMAIN-SUFFIX,adinall.com,AdBlock 173 | - DOMAIN-SUFFIX,adinfuse.com,AdBlock 174 | - DOMAIN-SUFFIX,adjust.com,AdBlock 175 | - DOMAIN-SUFFIX,adjust.io,AdBlock 176 | - DOMAIN-SUFFIX,adkmob.com,AdBlock 177 | - DOMAIN-SUFFIX,adlefee.com,AdBlock 178 | - DOMAIN-SUFFIX,admantx.com,AdBlock 179 | - DOMAIN-SUFFIX,admarketplace.net,AdBlock 180 | - DOMAIN-SUFFIX,admarvel.com,AdBlock 181 | - DOMAIN-SUFFIX,admaster.com.cn,AdBlock 182 | - DOMAIN-SUFFIX,admob.com,AdBlock 183 | - DOMAIN-SUFFIX,adnow.com,AdBlock 184 | - DOMAIN-SUFFIX,adnxs.com,AdBlock 185 | - DOMAIN-SUFFIX,adsafeprotected.com,AdBlock 186 | - DOMAIN-SUFFIX,adsota.com,AdBlock 187 | - DOMAIN-SUFFIX,ads-pixiv.net,AdBlock 188 | - DOMAIN-SUFFIX,adsrvr.org,AdBlock 189 | - DOMAIN-SUFFIX,ads-twitter.com,AdBlock 190 | - DOMAIN-SUFFIX,adswizz.com,AdBlock 191 | - DOMAIN-SUFFIX,adsymptotic.com,AdBlock 192 | - DOMAIN-SUFFIX,adtechus.com,AdBlock 193 | - DOMAIN-SUFFIX,adtilt.com,AdBlock 194 | - DOMAIN-SUFFIX,adtrue.com,AdBlock 195 | - DOMAIN-SUFFIX,AdBlock.com,AdBlock 196 | - DOMAIN-SUFFIX,advertnative.com,AdBlock 197 | - DOMAIN-SUFFIX,adview.cn,AdBlock 198 | - DOMAIN-SUFFIX,adxpansion.com,AdBlock 199 | - DOMAIN-SUFFIX,adxvip.com,AdBlock 200 | - DOMAIN-SUFFIX,aerserv.com,AdBlock 201 | - DOMAIN-SUFFIX,agkn.com,AdBlock 202 | - DOMAIN-SUFFIX,alipaylog.com,AdBlock 203 | - DOMAIN-SUFFIX,amazon-adsystem.com,AdBlock 204 | - DOMAIN-SUFFIX,analysys.cn,AdBlock 205 | - DOMAIN-SUFFIX,app-adforce.jp,AdBlock 206 | - DOMAIN-SUFFIX,appads.com,AdBlock 207 | - DOMAIN-SUFFIX,appboy.com,AdBlock 208 | - DOMAIN-SUFFIX,appier.net,AdBlock 209 | - DOMAIN-SUFFIX,applift.com,AdBlock 210 | - DOMAIN-SUFFIX,applovin.com,AdBlock 211 | - DOMAIN-SUFFIX,appnext.com,AdBlock 212 | - DOMAIN-SUFFIX,appodealx.com,AdBlock 213 | - DOMAIN-SUFFIX,appsee.com,AdBlock 214 | - DOMAIN-SUFFIX,appsflyer.com,AdBlock 215 | - DOMAIN-SUFFIX,apptentive.com,AdBlock 216 | - DOMAIN-SUFFIX,apptornado.com,AdBlock 217 | - DOMAIN-SUFFIX,atdmt.com,AdBlock 218 | - DOMAIN-SUFFIX,atwola.com,AdBlock 219 | - DOMAIN-SUFFIX,betrad.com,AdBlock 220 | - DOMAIN-SUFFIX,bidswitch.com,AdBlock 221 | - DOMAIN-SUFFIX,bjytgw.com,AdBlock 222 | - DOMAIN-SUFFIX,bttrack.com,AdBlock 223 | - DOMAIN-SUFFIX,bxmns.com,AdBlock 224 | - DOMAIN-SUFFIX,cappumedia.com,AdBlock 225 | - DOMAIN-SUFFIX,celtra.com,AdBlock 226 | - DOMAIN-SUFFIX,cferw.com,AdBlock 227 | - DOMAIN-SUFFIX,chartbeat.net,AdBlock 228 | - DOMAIN-SUFFIX,chartboost.com,AdBlock 229 | - DOMAIN-SUFFIX,chitika.com,AdBlock 230 | - DOMAIN-SUFFIX,clickhubs.com,AdBlock 231 | - DOMAIN-SUFFIX,clickintext.com,AdBlock 232 | - DOMAIN-SUFFIX,clickintext.net,AdBlock 233 | - DOMAIN-SUFFIX,cloudmobi.net,AdBlock 234 | - DOMAIN-SUFFIX,cnadnet.com,AdBlock 235 | - DOMAIN-SUFFIX,cnzz.com,AdBlock 236 | - DOMAIN-SUFFIX,cocounion.com,AdBlock 237 | - DOMAIN-SUFFIX,conversantmedia.com,AdBlock 238 | - DOMAIN-SUFFIX,conviva.com,AdBlock 239 | - DOMAIN-SUFFIX,criteo.com,AdBlock 240 | - DOMAIN-SUFFIX,crwdcntrl.net,AdBlock 241 | - DOMAIN-SUFFIX,ctrmi.com,AdBlock 242 | - DOMAIN-SUFFIX,demdex.net,AdBlock 243 | - DOMAIN-SUFFIX,dianomi.com,AdBlock 244 | - DOMAIN-SUFFIX,digitru.st,AdBlock 245 | - DOMAIN-SUFFIX,dtscout.com,AdBlock 246 | - DOMAIN-SUFFIX,duapps.com,AdBlock 247 | - DOMAIN-SUFFIX,effectivemeasure.net,AdBlock 248 | - DOMAIN-SUFFIX,endpo.in,AdBlock 249 | - DOMAIN-SUFFIX,eum-appdynamics.com,AdBlock 250 | - DOMAIN-SUFFIX,exoclick.com,AdBlock 251 | - DOMAIN-SUFFIX,exosrv.com,AdBlock 252 | - DOMAIN-SUFFIX,exponential.com,AdBlock 253 | - DOMAIN-SUFFIX,exposebox.com,AdBlock 254 | - DOMAIN-SUFFIX,eyeota.net,AdBlock 255 | - DOMAIN-SUFFIX,eyeviewads.com,AdBlock 256 | - DOMAIN-SUFFIX,flurry.com,AdBlock 257 | - DOMAIN-SUFFIX,fwmrm.net,AdBlock 258 | - DOMAIN-SUFFIX,getrockerbox.com,AdBlock 259 | - DOMAIN-SUFFIX,go2cloud.org,AdBlock 260 | - DOMAIN-SUFFIX,go-mpulse.net,AdBlock 261 | ... -------------------------------------------------------------------------------- /ProxyStationService.Tests/Parser/Fixtures/Clash.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | port: 7890 3 | socks-port: 7891 4 | allow-lan: true 5 | mode: Rule 6 | log-level: silent 7 | external-controller: 0.0.0.0:9090 8 | dns: 9 | enable: true 10 | ipv6: false 11 | listen: 0.0.0.0:53 12 | enhanced-mode: redir-host 13 | nameserver: 14 | - 117.50.10.10 15 | - 119.29.29.29 16 | - 223.5.5.5 17 | - tls://dns.rubyfish.cn:853 18 | fallback: 19 | - tls://1.1.1.1:853 20 | - tls://1.0.0.1:853 21 | - tls://dns.google:853 22 | Proxy: 23 | - name: "\U0001F1FA\U0001F1F8 \u4E2D\u56FD\u4E0A\u6D77 -> \u7F8E\u56FD 01 | IPLC" 24 | type: ss 25 | server: gg.gol.proxy 26 | port: 152 27 | cipher: chacha20-ietf-poly1305 28 | password: asdbasv 29 | udp: true 30 | obfs: tls 31 | obfs-host: adfadfads 32 | - name: "\U0001F1FA\U0001F1F8 \u4E2D\u56FD\u4E0A\u6D77 -> \u7F8E\u56FD 01 | IPLC" 33 | type: ss 34 | server: gg.gol.proxy 35 | port: 152 36 | cipher: chacha20-ietf-poly1305 37 | password: asdbasv 38 | plugin: obfs 39 | plugin-opts: 40 | mode: tls 41 | host: asdfasdf 42 | - name: "ss3" 43 | type: ss 44 | server: server 45 | port: 443 46 | cipher: AEAD_CHACHA20_POLY1305 47 | password: "password" 48 | plugin: v2ray-plugin 49 | plugin-opts: 50 | mode: websocket 51 | tls: true 52 | skip-cert-verify: true 53 | host: bing.com 54 | path: "/" 55 | headers: 56 | custom: value 57 | foo: bar 58 | - name: "ss5" 59 | type: ss 60 | server: server 61 | port: 443 62 | cipher: AEAD_CHACHA20_POLY1305 63 | password: "password" 64 | 65 | Rule: 66 | - DOMAIN-SUFFIX,edgedatg.com,GlobalTV 67 | - DOMAIN-SUFFIX,go.com,GlobalTV 68 | - DOMAIN,linear-abematv.akamaized.net,GlobalTV 69 | - DOMAIN-SUFFIX,abema.io,GlobalTV 70 | - DOMAIN-SUFFIX,abema.tv,GlobalTV 71 | - DOMAIN-SUFFIX,akamaized.net,GlobalTV 72 | - DOMAIN-SUFFIX,ameba.jp,GlobalTV 73 | - DOMAIN-SUFFIX,hayabusa.io,GlobalTV 74 | - DOMAIN-SUFFIX,aiv-cdn.net,GlobalTV 75 | - DOMAIN-SUFFIX,amazonaws.com,GlobalTV 76 | - DOMAIN-SUFFIX,amazonvideo.com,GlobalTV 77 | - DOMAIN-SUFFIX,llnwd.net,GlobalTV 78 | - DOMAIN-SUFFIX,bahamut.com.tw,GlobalTV 79 | - DOMAIN-SUFFIX,gamer.com.tw,GlobalTV 80 | - DOMAIN-SUFFIX,hinet.net,GlobalTV 81 | - DOMAIN-KEYWORD,bbcfmt,GlobalTV 82 | - DOMAIN-KEYWORD,co.uk,GlobalTV 83 | - DOMAIN-KEYWORD,uk-live,GlobalTV 84 | - DOMAIN-SUFFIX,bbc.co,GlobalTV 85 | - DOMAIN-SUFFIX,bbc.co.uk,GlobalTV 86 | - DOMAIN-SUFFIX,bbc.com,GlobalTV 87 | - DOMAIN-SUFFIX,bbci.co,GlobalTV 88 | - DOMAIN-SUFFIX,bbci.co.uk,GlobalTV 89 | - DOMAIN-SUFFIX,chocotv.com.tw,GlobalTV 90 | - DOMAIN-KEYWORD,epicgames,GlobalTV 91 | - DOMAIN-SUFFIX,helpshift.com,GlobalTV 92 | - DOMAIN-KEYWORD,foxplus,GlobalTV 93 | - DOMAIN-SUFFIX,config.fox.com,GlobalTV 94 | - DOMAIN-SUFFIX,emome.net,GlobalTV 95 | - DOMAIN-SUFFIX,fox.com,GlobalTV 96 | - DOMAIN-SUFFIX,foxdcg.com,GlobalTV 97 | - DOMAIN-SUFFIX,foxnow.com,GlobalTV 98 | - DOMAIN-SUFFIX,foxplus.com,GlobalTV 99 | - DOMAIN-SUFFIX,foxplay.com,GlobalTV 100 | - DOMAIN-SUFFIX,ipinfo.io,GlobalTV 101 | - DOMAIN-SUFFIX,mstage.io,GlobalTV 102 | - DOMAIN-SUFFIX,now.com,GlobalTV 103 | - DOMAIN-SUFFIX,theplatform.com,GlobalTV 104 | - DOMAIN-SUFFIX,urlload.net,GlobalTV 105 | - DOMAIN-SUFFIX,execute-api.ap-southeast-1.amazonaws.com,GlobalTV 106 | - DOMAIN-SUFFIX,hbo.com,GlobalTV 107 | - DOMAIN-SUFFIX,hboasia.com,GlobalTV 108 | - DOMAIN-SUFFIX,hbogo.com,GlobalTV 109 | - DOMAIN-SUFFIX,hbogoasia.hk,GlobalTV 110 | - DOMAIN-SUFFIX,happyon.jp,GlobalTV 111 | - DOMAIN-SUFFIX,hulu.com,GlobalTV 112 | - DOMAIN-SUFFIX,huluim.com,GlobalTV 113 | - DOMAIN-SUFFIX,hulustream.com,GlobalTV 114 | - DOMAIN-SUFFIX,imkan.tv,GlobalTV 115 | - DOMAIN-SUFFIX,joox.com,GlobalTV 116 | - DOMAIN-KEYWORD,nowtv100,GlobalTV 117 | - DOMAIN-KEYWORD,rthklive,GlobalTV 118 | - DOMAIN-SUFFIX,mytvsuper.com,GlobalTV 119 | - DOMAIN-SUFFIX,tvb.com,GlobalTV 120 | - DOMAIN-SUFFIX,netflix.com,GlobalTV 121 | - DOMAIN-SUFFIX,netflix.net,GlobalTV 122 | - DOMAIN-SUFFIX,nflxext.com,GlobalTV 123 | - DOMAIN-SUFFIX,nflximg.com,GlobalTV 124 | - DOMAIN-SUFFIX,nflximg.net,GlobalTV 125 | - DOMAIN-SUFFIX,nflxso.net,GlobalTV 126 | - DOMAIN-SUFFIX,nflxvideo.net,GlobalTV 127 | - DOMAIN-SUFFIX,pandora.com,GlobalTV 128 | - DOMAIN-SUFFIX,sky.com,GlobalTV 129 | - DOMAIN-SUFFIX,skygo.co.nz,GlobalTV 130 | - DOMAIN-KEYWORD,spotify,GlobalTV 131 | - DOMAIN-SUFFIX,scdn.co,GlobalTV 132 | - DOMAIN-SUFFIX,spoti.fi,GlobalTV 133 | - DOMAIN-SUFFIX,viu.tv,GlobalTV 134 | - DOMAIN-KEYWORD,youtube,GlobalTV 135 | - DOMAIN-SUFFIX,googlevideo.com,GlobalTV 136 | - DOMAIN-SUFFIX,gvt2.com,GlobalTV 137 | - DOMAIN-SUFFIX,youtu.be,GlobalTV 138 | - DOMAIN-KEYWORD,bilibili,AsianTV 139 | - DOMAIN-SUFFIX,acg.tv,AsianTV 140 | - DOMAIN-SUFFIX,acgvideo.com,AsianTV 141 | - DOMAIN-SUFFIX,b23.tv,AsianTV 142 | - DOMAIN-SUFFIX,biliapi.com,AsianTV 143 | - DOMAIN-SUFFIX,biliapi.net,AsianTV 144 | - DOMAIN-SUFFIX,bilibili.com,AsianTV 145 | - DOMAIN-SUFFIX,biligame.com,AsianTV 146 | - DOMAIN-SUFFIX,biligame.net,AsianTV 147 | - DOMAIN-SUFFIX,hdslb.com,AsianTV 148 | - DOMAIN-SUFFIX,im9.com,AsianTV 149 | - DOMAIN-KEYWORD,qiyi,AsianTV 150 | - DOMAIN-SUFFIX,qy.net,AsianTV 151 | - DOMAIN-SUFFIX,api.mob.app.letv.com,AsianTV 152 | - DOMAIN-SUFFIX,163yun.com,AsianTV 153 | - DOMAIN-SUFFIX,music.126.net,AsianTV 154 | - DOMAIN-SUFFIX,music.163.com,AsianTV 155 | - DOMAIN-SUFFIX,vv.video.qq.com,AsianTV 156 | - DOMAIN,analytics.google.com,Proxy 157 | - DOMAIN,analyticsinsights-pa.googleapis.com,Proxy 158 | - DOMAIN,analyticsreporting.googleapis.com,Proxy 159 | - DOMAIN-SUFFIX,vd.l.qq.com,Domestic 160 | - DOMAIN-KEYWORD,adservice,AdBlock 161 | - DOMAIN-KEYWORD,analytics,AdBlock 162 | - DOMAIN-KEYWORD,analysis,AdBlock 163 | - DOMAIN-SUFFIX,3lift.com,AdBlock 164 | - DOMAIN-SUFFIX,4006825178.com,AdBlock 165 | - DOMAIN-SUFFIX,51.la,AdBlock 166 | - DOMAIN-SUFFIX,550tg.com,AdBlock 167 | - DOMAIN-SUFFIX,56txs4.com,AdBlock 168 | - DOMAIN-SUFFIX,ad373.com,AdBlock 169 | - DOMAIN-SUFFIX,ad4screen.com,AdBlock 170 | - DOMAIN-SUFFIX,ad-brix.com,AdBlock 171 | - DOMAIN-SUFFIX,adcolony.com,AdBlock 172 | - DOMAIN-SUFFIX,adform.net,AdBlock 173 | - DOMAIN-SUFFIX,adinall.com,AdBlock 174 | - DOMAIN-SUFFIX,adinfuse.com,AdBlock 175 | - DOMAIN-SUFFIX,adjust.com,AdBlock 176 | - DOMAIN-SUFFIX,adjust.io,AdBlock 177 | - DOMAIN-SUFFIX,adkmob.com,AdBlock 178 | - DOMAIN-SUFFIX,adlefee.com,AdBlock 179 | - DOMAIN-SUFFIX,admantx.com,AdBlock 180 | - DOMAIN-SUFFIX,admarketplace.net,AdBlock 181 | - DOMAIN-SUFFIX,admarvel.com,AdBlock 182 | - DOMAIN-SUFFIX,admaster.com.cn,AdBlock 183 | - DOMAIN-SUFFIX,admob.com,AdBlock 184 | - DOMAIN-SUFFIX,adnow.com,AdBlock 185 | - DOMAIN-SUFFIX,adnxs.com,AdBlock 186 | - DOMAIN-SUFFIX,adsafeprotected.com,AdBlock 187 | - DOMAIN-SUFFIX,adsota.com,AdBlock 188 | - DOMAIN-SUFFIX,ads-pixiv.net,AdBlock 189 | - DOMAIN-SUFFIX,adsrvr.org,AdBlock 190 | - DOMAIN-SUFFIX,ads-twitter.com,AdBlock 191 | - DOMAIN-SUFFIX,adswizz.com,AdBlock 192 | - DOMAIN-SUFFIX,adsymptotic.com,AdBlock 193 | - DOMAIN-SUFFIX,adtechus.com,AdBlock 194 | - DOMAIN-SUFFIX,adtilt.com,AdBlock 195 | - DOMAIN-SUFFIX,adtrue.com,AdBlock 196 | - DOMAIN-SUFFIX,AdBlock.com,AdBlock 197 | - DOMAIN-SUFFIX,advertnative.com,AdBlock 198 | - DOMAIN-SUFFIX,adview.cn,AdBlock 199 | - DOMAIN-SUFFIX,adxpansion.com,AdBlock 200 | - DOMAIN-SUFFIX,adxvip.com,AdBlock 201 | - DOMAIN-SUFFIX,aerserv.com,AdBlock 202 | - DOMAIN-SUFFIX,agkn.com,AdBlock 203 | - DOMAIN-SUFFIX,alipaylog.com,AdBlock 204 | - DOMAIN-SUFFIX,amazon-adsystem.com,AdBlock 205 | - DOMAIN-SUFFIX,analysys.cn,AdBlock 206 | - DOMAIN-SUFFIX,app-adforce.jp,AdBlock 207 | - DOMAIN-SUFFIX,appads.com,AdBlock 208 | - DOMAIN-SUFFIX,appboy.com,AdBlock 209 | - DOMAIN-SUFFIX,appier.net,AdBlock 210 | - DOMAIN-SUFFIX,applift.com,AdBlock 211 | - DOMAIN-SUFFIX,applovin.com,AdBlock 212 | - DOMAIN-SUFFIX,appnext.com,AdBlock 213 | - DOMAIN-SUFFIX,appodealx.com,AdBlock 214 | - DOMAIN-SUFFIX,appsee.com,AdBlock 215 | - DOMAIN-SUFFIX,appsflyer.com,AdBlock 216 | - DOMAIN-SUFFIX,apptentive.com,AdBlock 217 | - DOMAIN-SUFFIX,apptornado.com,AdBlock 218 | - DOMAIN-SUFFIX,atdmt.com,AdBlock 219 | - DOMAIN-SUFFIX,atwola.com,AdBlock 220 | - DOMAIN-SUFFIX,betrad.com,AdBlock 221 | - DOMAIN-SUFFIX,bidswitch.com,AdBlock 222 | - DOMAIN-SUFFIX,bjytgw.com,AdBlock 223 | - DOMAIN-SUFFIX,bttrack.com,AdBlock 224 | - DOMAIN-SUFFIX,bxmns.com,AdBlock 225 | - DOMAIN-SUFFIX,cappumedia.com,AdBlock 226 | - DOMAIN-SUFFIX,celtra.com,AdBlock 227 | - DOMAIN-SUFFIX,cferw.com,AdBlock 228 | - DOMAIN-SUFFIX,chartbeat.net,AdBlock 229 | - DOMAIN-SUFFIX,chartboost.com,AdBlock 230 | - DOMAIN-SUFFIX,chitika.com,AdBlock 231 | - DOMAIN-SUFFIX,clickhubs.com,AdBlock 232 | - DOMAIN-SUFFIX,clickintext.com,AdBlock 233 | - DOMAIN-SUFFIX,clickintext.net,AdBlock 234 | - DOMAIN-SUFFIX,cloudmobi.net,AdBlock 235 | - DOMAIN-SUFFIX,cnadnet.com,AdBlock 236 | - DOMAIN-SUFFIX,cnzz.com,AdBlock 237 | - DOMAIN-SUFFIX,cocounion.com,AdBlock 238 | - DOMAIN-SUFFIX,conversantmedia.com,AdBlock 239 | - DOMAIN-SUFFIX,conviva.com,AdBlock 240 | - DOMAIN-SUFFIX,criteo.com,AdBlock 241 | - DOMAIN-SUFFIX,crwdcntrl.net,AdBlock 242 | - DOMAIN-SUFFIX,ctrmi.com,AdBlock 243 | - DOMAIN-SUFFIX,demdex.net,AdBlock 244 | - DOMAIN-SUFFIX,dianomi.com,AdBlock 245 | - DOMAIN-SUFFIX,digitru.st,AdBlock 246 | - DOMAIN-SUFFIX,dtscout.com,AdBlock 247 | - DOMAIN-SUFFIX,duapps.com,AdBlock 248 | - DOMAIN-SUFFIX,effectivemeasure.net,AdBlock 249 | - DOMAIN-SUFFIX,endpo.in,AdBlock 250 | - DOMAIN-SUFFIX,eum-appdynamics.com,AdBlock 251 | - DOMAIN-SUFFIX,exoclick.com,AdBlock 252 | - DOMAIN-SUFFIX,exosrv.com,AdBlock 253 | - DOMAIN-SUFFIX,exponential.com,AdBlock 254 | - DOMAIN-SUFFIX,exposebox.com,AdBlock 255 | - DOMAIN-SUFFIX,eyeota.net,AdBlock 256 | - DOMAIN-SUFFIX,eyeviewads.com,AdBlock 257 | - DOMAIN-SUFFIX,flurry.com,AdBlock 258 | - DOMAIN-SUFFIX,fwmrm.net,AdBlock 259 | - DOMAIN-SUFFIX,getrockerbox.com,AdBlock 260 | - DOMAIN-SUFFIX,go2cloud.org,AdBlock 261 | - DOMAIN-SUFFIX,go-mpulse.net,AdBlock 262 | ... -------------------------------------------------------------------------------- /ProxyStationService/HttpTrigger/Functions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Text; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Http; 6 | using Microsoft.AspNetCore.Mvc; 7 | using Microsoft.Azure.WebJobs; 8 | using Microsoft.Azure.WebJobs.Extensions.Http; 9 | using Microsoft.Extensions.Logging; 10 | using ProxyStation.ProfileParser; 11 | using ProxyStation.Model; 12 | using ProxyStation.Util; 13 | using System.Collections.Generic; 14 | using System.Web; 15 | 16 | namespace ProxyStation.HttpTrigger 17 | { 18 | public static class Functions 19 | { 20 | public static IEnvironmentManager EnvironmentManager { get; set; } = new EnvironmentManager(); 21 | 22 | private static IDownloader downloader; 23 | 24 | public static IDownloader Downloader 25 | { 26 | get 27 | { 28 | if (Functions.downloader == null) 29 | { 30 | var useCache = Functions.EnvironmentManager.Get("USE_CACHE") == "1"; 31 | if (useCache) 32 | { 33 | var connectionString = Functions.EnvironmentManager.Get("AzureWebJobsStorage"); 34 | Functions.downloader = new Downloader(connectionString); 35 | } 36 | else 37 | { 38 | Functions.downloader = new Downloader(); 39 | } 40 | } 41 | 42 | return Functions.downloader; 43 | } 44 | set 45 | { 46 | Functions.downloader = value; 47 | } 48 | } 49 | #region Functions 50 | 51 | [FunctionName("GetTrain")] 52 | public static async Task GetTrain( 53 | [HttpTrigger(AuthorizationLevel.Anonymous, "GET", Route = "train/{profileName}/{typeName?}")] HttpRequest req, 54 | string profileName, 55 | string typeName, 56 | ILogger logger) 57 | { 58 | var templateUrlOrName = req.Query.ContainsKey("template") ? req.Query["template"].ToString() : String.Empty; 59 | var requestUrl = Functions.GetCurrentURL(req); 60 | 61 | ProfileFactory.SetEnvironmentManager(Functions.EnvironmentManager); 62 | TemplateFactory.SetEnvironmentManager(Functions.EnvironmentManager); 63 | 64 | // Parse target parser type 65 | IProfileParser targetProfileParser = ParserFactory.GetParser(typeName ?? "", logger, Functions.Downloader); 66 | if (targetProfileParser == null) 67 | { 68 | var userAgent = req.Headers["user-agent"]; 69 | var probableType = Functions.GuessTypeFromUserAgent(userAgent); 70 | targetProfileParser = ParserFactory.GetParser(probableType, logger, Functions.Downloader); 71 | logger.LogInformation("Attempt to guess target type from user agent, UserAgent={userAgent}, Result={targetType}", userAgent, targetProfileParser.GetType()); 72 | } 73 | 74 | // Get profile chain 75 | var profileChain = new List(); 76 | var nextProfileName = Misc.KebabCase2PascalCase(profileName); 77 | while (true) 78 | { 79 | var profile = ProfileFactory.Get(nextProfileName, logger); 80 | if (profile == null) 81 | { 82 | var chainString = Functions.ProfileChainToString(profileChain); 83 | if (!string.IsNullOrEmpty(chainString)) chainString += "->"; 84 | chainString += nextProfileName; 85 | 86 | logger.LogError($"Profile `{chainString}` is not found."); 87 | return new NotFoundResult(); 88 | } 89 | 90 | if (profileChain.Contains(profile)) 91 | { 92 | return new ForbidResult(); 93 | } 94 | 95 | profileChain.Add(profile); 96 | if (profile.Type != ProfileType.Alias) break; 97 | nextProfileName = profile.Source; 98 | } 99 | 100 | var sourceProfile = profileChain.Last(); 101 | var profileParser = ParserFactory.GetParser(sourceProfile.Type, logger, Functions.Downloader); 102 | if (profileParser == null) 103 | { 104 | logger.LogError($"Profile parser for {sourceProfile.Type} is not implemented! Complete profile alias chain is `{Functions.ProfileChainToString(profileChain)}`"); 105 | return new ForbidResult(); 106 | } 107 | 108 | // Download content and determine if original profile should be returned 109 | var profileContent = await sourceProfile.Download(logger, Functions.Downloader); 110 | if (targetProfileParser is NullParser) 111 | { 112 | if (!sourceProfile.AllowDirectAccess) 113 | { 114 | logger.LogError($"Original profile access is denied for profile `{Functions.ProfileChainToString(profileChain)}`."); 115 | return new ForbidResult(); 116 | } 117 | 118 | logger.LogInformation("Return original profile"); 119 | return new FileContentResult(Encoding.UTF8.GetBytes(profileContent), "text/plain; charset=UTF-8") 120 | { 121 | FileDownloadName = profileChain.First().Name + profileParser.ExtName(), 122 | }; 123 | } 124 | 125 | // Download template, parse profile and apply filters 126 | var template = await Functions.GetTemplate(logger, templateUrlOrName); 127 | var servers = profileParser.Parse(profileContent); 128 | logger.LogInformation($"Download profile `{Functions.ProfileChainToString(profileChain)}` and get {servers.Length} servers"); 129 | foreach (var profile in profileChain.AsEnumerable().Reverse()) 130 | { 131 | foreach (var filter in profile.Filters) 132 | { 133 | servers = filter.Do(servers, logger); 134 | logger.LogInformation($"Apply filter `{filter.GetType()} from profile `{profile.Name}` and get {servers.Length} servers"); 135 | if (servers.Length == 0) break; 136 | } 137 | if (servers.Length == 0) break; 138 | } 139 | if (servers.Length == 0) 140 | { 141 | logger.LogError($"There are no available servers left. Complete profile alias chain is `{Functions.ProfileChainToString(profileChain)}`"); 142 | return new NoContentResult(); 143 | } 144 | 145 | // Encode profile 146 | logger.LogInformation($"{servers.Length} will be encoded"); 147 | var options = targetProfileParser switch 148 | { 149 | SurgeParser _ => new SurgeEncodeOptions() 150 | { 151 | ProfileURL = requestUrl + (string.IsNullOrEmpty(template) ? "" : $"?template={HttpUtility.UrlEncode(templateUrlOrName)}") 152 | }, 153 | QuantumultXParser _ => new QuantumultXEncodeOptions() 154 | { 155 | QuantumultXListUrl = Functions.GetCurrentURL(req) + "-list", 156 | }, 157 | ClashParser _ => new ClashEncodeOptions() 158 | { 159 | ClashProxyProviderUrl = Functions.GetCurrentURL(req) + "-proxy-provider", 160 | }, 161 | _ => new EncodeOptions(), 162 | }; 163 | options.Template = template; 164 | options.ProfileName = profileChain.First().Name; 165 | 166 | try 167 | { 168 | var newProfile = targetProfileParser.Encode(options, servers, out Server[] encodedServer); 169 | if (encodedServer.Length == 0) 170 | { 171 | return new NoContentResult(); 172 | } 173 | 174 | return new FileContentResult(Encoding.UTF8.GetBytes(newProfile), "text/plain; charset=UTF-8") 175 | { 176 | FileDownloadName = profileChain.First().Name + targetProfileParser.ExtName(), 177 | }; 178 | } 179 | catch (InvalidTemplateException) 180 | { 181 | return new BadRequestResult(); 182 | } 183 | } 184 | 185 | #endregion Functions 186 | 187 | public static async Task GetTemplate(ILogger logger, string templateUrlOrName) 188 | { 189 | if (!string.IsNullOrEmpty(templateUrlOrName) && !templateUrlOrName.StartsWith("https://")) 190 | { 191 | templateUrlOrName = TemplateFactory.GetTemplateUrl(templateUrlOrName); 192 | } 193 | 194 | if (!string.IsNullOrEmpty(templateUrlOrName) && templateUrlOrName.StartsWith("https://")) 195 | { 196 | return await Functions.Downloader.Download(logger, templateUrlOrName); 197 | } 198 | return null; 199 | } 200 | 201 | public static string ProfileChainToString(IEnumerable profileChain) 202 | { 203 | return string.Join("->", profileChain.Select(s => s.Name)); 204 | } 205 | 206 | public static string GetCurrentURL(HttpRequest req) => $"{req.Scheme}://{req.Host}{req.Path}{req.QueryString}"; 207 | 208 | public static ProfileType GuessTypeFromUserAgent(string userAgent) 209 | { 210 | userAgent = userAgent.ToLower(); 211 | if (userAgent.Contains("surge")) return ProfileType.Surge; 212 | else if (userAgent.Contains("clash")) return ProfileType.Clash; 213 | return ProfileType.General; 214 | } 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /ProxyStationService/Parser/GeneralParser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Web; 6 | using Microsoft.AspNetCore.Http.Extensions; 7 | using Microsoft.Extensions.Logging; 8 | using ProxyStation.Model; 9 | 10 | namespace ProxyStation.ProfileParser 11 | { 12 | public class GeneralParser : IProfileParser 13 | { 14 | private ILogger logger; 15 | 16 | public GeneralParser(ILogger logger) 17 | { 18 | this.logger = logger; 19 | } 20 | 21 | public Server[] Parse(string profile) 22 | { 23 | var bytes = WebSafeBase64Decode(profile.Trim()); 24 | var plain = Encoding.UTF8.GetString(bytes); 25 | var URIs = plain.Split('\n'); 26 | List servers = new List(); 27 | 28 | foreach (var uri in URIs) 29 | { 30 | if (uri.StartsWith("ss://")) servers.Add(ParseShadowsocksURI(uri)); 31 | else if (uri.StartsWith("ssr://")) servers.Add(ParseShadowsocksRURI(uri)); 32 | else if (uri.StartsWith("vmess://")) 33 | { 34 | // a common but not official standard scheme comes here 35 | // https://github.com/2dust/v2rayN/wiki/%E5%88%86%E4%BA%AB%E9%93%BE%E6%8E%A5%E6%A0%BC%E5%BC%8F%E8%AF%B4%E6%98%8E(ver-2) 36 | } 37 | else 38 | { 39 | // TODO: log error 40 | } 41 | } 42 | 43 | return servers.ToArray(); 44 | } 45 | 46 | /// 47 | /// Follow scheme https://shadowsocks.org/en/spec/SIP002-URI-Scheme.html 48 | /// to parse a given shadowsocks server uri. 49 | /// 50 | internal ShadowsocksServer ParseShadowsocksURI(string ssURI) 51 | { 52 | var u = new Uri(ssURI); 53 | var server = new ShadowsocksServer(); 54 | 55 | // UserInfo: WebSafe Base64(method:password) 56 | var plainUserInfo = Encoding.UTF8.GetString(WebSafeBase64Decode(HttpUtility.UrlDecode(u.UserInfo))).Split(':', 2); 57 | server.Method = plainUserInfo[0]; 58 | server.Password = plainUserInfo.ElementAtOrDefault(1); 59 | 60 | // Host & Port 61 | server.Host = u.Host; 62 | server.Port = u.Port; 63 | 64 | // Name 65 | if (u.Fragment.StartsWith("#")) 66 | server.Name = HttpUtility.UrlDecode(u.Fragment.Substring(1)); 67 | 68 | // Plugin 69 | var otherParams = HttpUtility.ParseQueryString(u.Query); 70 | var plugin = (otherParams.Get("plugin") ?? "").Trim(); 71 | var pluginInfos = plugin.Split(";"); 72 | switch (pluginInfos[0].Trim()) 73 | { 74 | case "simple-obfs": 75 | case "obfs-local": 76 | var options = new SimpleObfsPluginOptions(); 77 | server.PluginOptions = options; 78 | 79 | foreach (var info in pluginInfos) 80 | { 81 | var trimedInfo = info.Trim(); 82 | if (trimedInfo.StartsWith("obfs=")) 83 | { 84 | if (SimpleObfsPluginOptions.TryParseMode(trimedInfo.Substring("obfs=".Length).Trim(), out SimpleObfsPluginMode mode)) 85 | { 86 | options.Mode = mode; 87 | } 88 | } 89 | else if (string.IsNullOrEmpty(options.Host) && trimedInfo.StartsWith("obfs-host=")) 90 | { 91 | options.Host = trimedInfo.Substring("obfs-host=".Length); 92 | } 93 | else 94 | { 95 | // TODO: log 96 | } 97 | } 98 | break; 99 | } 100 | 101 | 102 | 103 | return server; 104 | } 105 | 106 | 107 | /// 108 | /// Follow scheme https://github.com/shadowsocksr-backup/shadowsocks-rss/wiki/SSR-QRcode-scheme 109 | /// to parse a given shadowsocksR server uri. 110 | /// 111 | ShadowsocksRServer ParseShadowsocksRURI(string ssrURI) 112 | { 113 | ssrURI = ssrURI.Substring("ssr://".Length); 114 | var splitted = Encoding.UTF8.GetString(WebSafeBase64Decode(HttpUtility.UrlDecode(ssrURI))).Split("/?"); 115 | 116 | // host:port:protocol:method:obfs:base64pass 117 | var serverInfo = splitted[0].Split(":", 6); 118 | var server = new ShadowsocksRServer() 119 | { 120 | Host = serverInfo[0], 121 | Port = Convert.ToInt32(serverInfo.ElementAtOrDefault(1) ?? "0"), 122 | Protocol = serverInfo.ElementAtOrDefault(2), 123 | Method = serverInfo.ElementAtOrDefault(3), 124 | Obfuscation = serverInfo.ElementAtOrDefault(4), 125 | Password = Encoding.ASCII.GetString(Convert.FromBase64String(serverInfo.ElementAtOrDefault(5) ?? "")), 126 | }; 127 | 128 | // /?obfsparam=base64param&protoparam=base64param&remarks=base64remarks&group=base64group&udpport=0&uot=0 129 | var otherInfos = splitted.ElementAtOrDefault(1); 130 | if (!string.IsNullOrEmpty(otherInfos)) 131 | { 132 | var qs = HttpUtility.ParseQueryString(otherInfos); 133 | server.ProtocolParameter = qs["protoparam"] != null ? Encoding.UTF8.GetString(WebSafeBase64Decode(qs["protoparam"])) : null; 134 | server.ObfuscationParameter = qs["obfsparam"] != null ? Encoding.UTF8.GetString(WebSafeBase64Decode(qs["obfsparam"])) : null; 135 | server.Name = qs["remarks"] != null ? Encoding.UTF8.GetString(WebSafeBase64Decode(qs["remarks"])) : null; 136 | server.UDPOverTCP = qs["uot"] != null && qs["uot"] != "0"; 137 | int UDPPort; 138 | if (Int32.TryParse(qs["udpport"], out UDPPort)) server.UDPPort = UDPPort; 139 | } 140 | 141 | return server; 142 | } 143 | 144 | private string EncodeShadowsocksServer(ShadowsocksServer server, string groupName = null) 145 | { 146 | var userInfo = WebSafeBase64Encode(Encoding.UTF8.GetBytes($"{server.Method}:{server.Password}")); 147 | var uri = new UriBuilder(); 148 | var queryBuilder = new QueryBuilder(); 149 | 150 | uri.UserName = userInfo; 151 | uri.Host = server.Host; 152 | uri.Port = server.Port; 153 | uri.Path = "/"; 154 | uri.Fragment = HttpUtility.UrlEncode(server.Name).Replace("+", "%20"); 155 | uri.Scheme = "ss"; 156 | 157 | if (server.PluginOptions is SimpleObfsPluginOptions options) 158 | { 159 | var obfsHost = string.IsNullOrEmpty(options.Host) ? Constant.ObfsucationHost : options.Host; 160 | queryBuilder.Add("plugin", $"obfs-local;obfs={options.Mode.ToString().ToLower()};obfs-host={obfsHost}"); 161 | } 162 | 163 | if (!string.IsNullOrEmpty(groupName)) 164 | { 165 | queryBuilder.Add("name", WebSafeBase64Encode(Encoding.UTF8.GetBytes(groupName))); 166 | } 167 | 168 | uri.Query = queryBuilder.ToString().Replace(";", "%3B"); 169 | 170 | return uri.ToString(); 171 | } 172 | 173 | private string EncodeShadowsocksRServer(ShadowsocksRServer server, string groupName = null) 174 | { 175 | // base64(host:port:protocol:method:obfs:base64pass/?group=base64group) 176 | var serverInfo = $"{server.Host}:{server.Port}:{server.Protocol}:{server.Method}:{server.Obfuscation}"; 177 | 178 | var otherInfos = new List() { 179 | "remarks=" + WebSafeBase64Encode(Encoding.UTF8.GetBytes(server.Name)) 180 | }; 181 | if (!string.IsNullOrEmpty(server.ObfuscationParameter)) 182 | otherInfos.Add("obfsparam=" + WebSafeBase64Encode(Encoding.UTF8.GetBytes(server.ObfuscationParameter))); 183 | if (!string.IsNullOrEmpty(server.ProtocolParameter)) 184 | otherInfos.Add("protoparam=" + WebSafeBase64Encode(Encoding.UTF8.GetBytes(server.ProtocolParameter))); 185 | if (server.UDPPort > 0) 186 | otherInfos.Add($"udpport={server.UDPPort}"); 187 | if (server.UDPOverTCP) 188 | otherInfos.Add("uot=1"); 189 | if (!string.IsNullOrEmpty(groupName)) 190 | otherInfos.Add("group=" + WebSafeBase64Encode(Encoding.UTF8.GetBytes(groupName))); 191 | 192 | return "ssr://" + WebSafeBase64Encode(Encoding.UTF8.GetBytes(serverInfo + "/?" + string.Join("&", otherInfos))); 193 | } 194 | 195 | public string Encode(EncodeOptions options, Server[] servers, out Server[] encodedServers) 196 | { 197 | var encodedServerList = new List(); 198 | var sb = new StringBuilder(); 199 | sb.AppendLine($"REMARKS={options.ProfileName}"); 200 | sb.AppendLine(""); 201 | 202 | foreach (var server in servers) 203 | { 204 | var serverLine = server switch 205 | { 206 | ShadowsocksServer ss => EncodeShadowsocksServer(ss), 207 | ShadowsocksRServer ssr => EncodeShadowsocksRServer(ssr), 208 | _ => null, 209 | }; 210 | 211 | if (string.IsNullOrWhiteSpace(serverLine)) 212 | { 213 | this.logger.LogInformation($"Server {server} is ignored."); 214 | } 215 | else 216 | { 217 | encodedServerList.Add(server); 218 | sb.AppendLine(serverLine); 219 | } 220 | } 221 | 222 | encodedServers = encodedServerList.ToArray(); 223 | return Convert.ToBase64String(Encoding.ASCII.GetBytes(sb.ToString())); 224 | } 225 | 226 | private static string WebSafeBase64Encode(byte[] plainText) 227 | { 228 | return Convert.ToBase64String(plainText) 229 | .TrimEnd('=') 230 | .Replace('+', '-') 231 | .Replace('/', '_'); 232 | } 233 | 234 | private static byte[] WebSafeBase64Decode(string encodedText) 235 | { 236 | string replacedText = encodedText.Replace('_', '/').Replace('-', '+'); 237 | switch (encodedText.Length % 4) 238 | { 239 | case 2: replacedText += "=="; break; 240 | case 3: replacedText += "="; break; 241 | } 242 | return Convert.FromBase64String(replacedText); 243 | } 244 | 245 | public string ExtName() 246 | { 247 | return ".conf"; 248 | } 249 | } 250 | } -------------------------------------------------------------------------------- /ProxyStationService/Parser/ClashParser.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using System.Linq; 4 | using Microsoft.Extensions.Logging; 5 | using ProxyStation.Model; 6 | using ProxyStation.ProfileParser.Template; 7 | using ProxyStation.Util; 8 | using YamlDotNet.RepresentationModel; 9 | using YamlDotNet.Serialization; 10 | 11 | namespace ProxyStation.ProfileParser 12 | { 13 | public class ClashEncodeOptions : EncodeOptions 14 | { 15 | public string ClashProxyProviderUrl { get; set; } 16 | } 17 | 18 | public class ClashParser : IProfileParser 19 | { 20 | private ILogger logger; 21 | 22 | public ClashParser(ILogger logger) 23 | { 24 | this.logger = logger; 25 | } 26 | 27 | public Server[] Parse(string profile) 28 | { 29 | using (var reader = new StringReader(profile)) 30 | { 31 | var yaml = new YamlStream(); 32 | yaml.Load(reader); 33 | 34 | var proxyNode = (yaml.Documents[0].RootNode as YamlMappingNode).Children["Proxy"]; 35 | return this.Parse(proxyNode); 36 | } 37 | } 38 | 39 | public Server[] Parse(YamlNode proxyNode) 40 | { 41 | var servers = new List(); 42 | 43 | var proxies = (proxyNode as YamlSequenceNode)?.Children.ToList(); 44 | if (proxies?.Count == 0) 45 | { 46 | return servers.ToArray(); 47 | } 48 | 49 | foreach (YamlMappingNode proxy in proxies) 50 | { 51 | switch (Yaml.GetStringOrDefaultFromYamlChildrenNode(proxy, "type")) 52 | { 53 | case "ss": 54 | var server = ParseShadowsocksServer(proxy); 55 | if (server != null) servers.Add(server); 56 | break; 57 | case "vmess": 58 | default: break; 59 | } 60 | } 61 | 62 | return servers.ToArray(); 63 | } 64 | 65 | public string Encode(EncodeOptions options, Server[] servers, out Server[] encodedServers) 66 | { 67 | var template = string.IsNullOrEmpty(options.Template) ? Clash.Template : options.Template; 68 | var deserializer = new DeserializerBuilder().Build(); 69 | var result = deserializer.Deserialize(template); 70 | 71 | var rootElement = result as Dictionary; 72 | if (rootElement == null || !rootElement.ContainsKey("proxy-provider")) 73 | { 74 | throw new InvalidTemplateException(); 75 | } 76 | 77 | var proxyProviderSlot = (rootElement.GetValueOrDefault("proxy-provider") as Dictionary) 78 | .FirstOrDefault(p => (p.Value as Dictionary)?.GetValueOrDefault("slot")?.ToString() == "true"); 79 | 80 | if (proxyProviderSlot.Equals(default(KeyValuePair))) 81 | { 82 | throw new InvalidTemplateException(); 83 | } 84 | this.InternalEncode(options, servers, out encodedServers); 85 | 86 | var opts = options as ClashEncodeOptions; 87 | (proxyProviderSlot.Value as Dictionary).Remove("slot"); 88 | (proxyProviderSlot.Value as Dictionary)["url"] = opts?.ClashProxyProviderUrl; 89 | var serializer = new SerializerBuilder().Build(); 90 | return serializer.Serialize(rootElement); 91 | } 92 | 93 | internal List> InternalEncode(EncodeOptions options, Server[] servers, out Server[] encodedServers) 94 | { 95 | var encodedServersList = new List(); 96 | var proxySettings = servers.Select(s => 97 | { 98 | if (s is ShadowsocksServer ss) 99 | { 100 | var proxy = new Dictionary() { 101 | { "name", ss.Name }, 102 | { "type", "ss" }, 103 | { "server", ss.Host }, 104 | { "port", ss.Port }, 105 | { "cipher", ss.Method }, 106 | { "password", ss.Password }, 107 | }; 108 | if (ss.UDPRelay) proxy.Add("udp", true); 109 | 110 | var pluginOptions = new Dictionary(); 111 | if (ss.PluginOptions is SimpleObfsPluginOptions obfsOptions) 112 | { 113 | pluginOptions.Add("mode", obfsOptions.Mode.ToString().ToLower()); 114 | pluginOptions.Add("host", string.IsNullOrEmpty(obfsOptions.Host) ? Constant.ObfsucationHost : obfsOptions.Host); 115 | proxy.Add("plugin", "obfs"); 116 | proxy.Add("plugin-opts", pluginOptions); 117 | } 118 | else if (ss.PluginOptions is V2RayPluginOptions v2rayOptions) 119 | { 120 | if (v2rayOptions.Mode == V2RayPluginMode.WebSocket) 121 | { 122 | pluginOptions.Add("mode", v2rayOptions.Mode.ToString().ToLower()); 123 | pluginOptions.Add("host", v2rayOptions.Host); 124 | pluginOptions.Add("path", string.IsNullOrEmpty(v2rayOptions.Path) ? "/" : v2rayOptions.Path); 125 | if (v2rayOptions.SkipCertVerification) pluginOptions.Add("skip-cert-verify", true); 126 | if (v2rayOptions.EnableTLS) pluginOptions.Add("tls", true); 127 | if (v2rayOptions.Headers.Count > 0) pluginOptions.Add("headers", v2rayOptions.Headers); 128 | } 129 | else 130 | { 131 | this.logger.LogError($"Clash doesn't support v2ray-plugin on QUIC. This server will be ignored"); 132 | return null; 133 | } 134 | proxy.Add("plugin", "v2ray-plugin"); 135 | proxy.Add("plugin-opts", pluginOptions); 136 | } 137 | 138 | encodedServersList.Add(s); 139 | return proxy; 140 | } 141 | else 142 | { 143 | this.logger.LogDebug($"Server {s} is ignored."); 144 | return null; 145 | } 146 | }).ToList(); 147 | encodedServers = encodedServersList.ToArray(); 148 | return proxySettings; 149 | } 150 | 151 | public string ExtName() => ".yaml"; 152 | 153 | public Server ParseShadowsocksServer(YamlMappingNode proxy) 154 | { 155 | string portString = Yaml.GetStringOrDefaultFromYamlChildrenNode(proxy, "port", "0"); 156 | int port; 157 | if (!int.TryParse(portString, out port)) 158 | { 159 | this.logger.LogError($"Invalid port: {port}."); 160 | return null; 161 | } 162 | 163 | var server = new ShadowsocksServer() 164 | { 165 | Port = port, 166 | Name = Yaml.GetStringOrDefaultFromYamlChildrenNode(proxy, "name"), 167 | Host = Yaml.GetStringOrDefaultFromYamlChildrenNode(proxy, "server"), 168 | Password = Yaml.GetStringOrDefaultFromYamlChildrenNode(proxy, "password"), 169 | Method = Yaml.GetStringOrDefaultFromYamlChildrenNode(proxy, "cipher"), 170 | UDPRelay = Yaml.GetTruthFromYamlChildrenNode(proxy, "udp"), 171 | }; 172 | 173 | YamlNode pluginOptionsNode; 174 | // refer to offical clash to parse plugin options 175 | // https://github.com/Dreamacro/clash/blob/34338e7107c1868124f8aab2446f6b71c9b0640f/adapters/outbound/shadowsocks.go#L135 176 | if (proxy.Children.TryGetValue("plugin-opts", out pluginOptionsNode) && pluginOptionsNode.NodeType == YamlNodeType.Mapping) 177 | { 178 | switch (Yaml.GetStringOrDefaultFromYamlChildrenNode(proxy, "plugin").ToLower()) 179 | { 180 | case "obfs": 181 | var simpleObfsModeString = Yaml.GetStringOrDefaultFromYamlChildrenNode(pluginOptionsNode, "mode"); 182 | var simpleObfsOptions = new SimpleObfsPluginOptions(); 183 | 184 | if (SimpleObfsPluginOptions.TryParseMode(simpleObfsModeString, out SimpleObfsPluginMode simpleObfsMode)) 185 | { 186 | simpleObfsOptions.Mode = simpleObfsMode; 187 | } 188 | else if (!string.IsNullOrWhiteSpace(simpleObfsModeString)) 189 | { 190 | this.logger.LogError($"Unsupported simple-obfs mode: {simpleObfsModeString}. This server will be ignored."); 191 | return null; 192 | } 193 | simpleObfsOptions.Host = Yaml.GetStringOrDefaultFromYamlChildrenNode(pluginOptionsNode, "host"); 194 | 195 | server.PluginOptions = simpleObfsOptions; 196 | break; 197 | case "v2ray-plugin": 198 | // also refer to official v2ray-plugin to parse v2ray-plugin options 199 | // https://github.com/shadowsocks/v2ray-plugin/blob/c7017f45bb1e12cf1e4b739bcb8f42f3eb8b22cd/main.go#L126 200 | var v2rayModeString = Yaml.GetStringOrDefaultFromYamlChildrenNode(pluginOptionsNode, "mode"); 201 | var options = new V2RayPluginOptions(); 202 | server.PluginOptions = options; 203 | 204 | if (V2RayPluginOptions.TryParseMode(v2rayModeString, out V2RayPluginMode v2rayMode)) 205 | { 206 | options.Mode = v2rayMode; 207 | } 208 | else 209 | { 210 | this.logger.LogError($"Unsupported v2ray-plugin mode: {v2rayModeString}. This server will be ignored."); 211 | return null; 212 | } 213 | 214 | options.Host = Yaml.GetStringOrDefaultFromYamlChildrenNode(pluginOptionsNode, "host"); 215 | options.Path = Yaml.GetStringOrDefaultFromYamlChildrenNode(pluginOptionsNode, "path"); 216 | options.EnableTLS = Yaml.GetTruthFromYamlChildrenNode(pluginOptionsNode, "tls"); 217 | options.SkipCertVerification = Yaml.GetTruthFromYamlChildrenNode(pluginOptionsNode, "skip-cert-verify"); 218 | options.Headers = new Dictionary(); 219 | 220 | YamlNode headersNode; 221 | if (!(pluginOptionsNode as YamlMappingNode).Children.TryGetValue("headers", out headersNode)) 222 | { 223 | break; 224 | } 225 | if (headersNode.NodeType != YamlNodeType.Mapping) 226 | { 227 | break; 228 | } 229 | 230 | foreach (var header in (headersNode as YamlMappingNode)) 231 | { 232 | if (header.Value.NodeType != YamlNodeType.Scalar) continue; 233 | options.Headers.Add((header.Key as YamlScalarNode).Value, (header.Value as YamlScalarNode).Value); 234 | } 235 | break; 236 | } 237 | } 238 | 239 | if (server.PluginOptions == null) 240 | { 241 | var simpleObfsModeString = Yaml.GetStringOrDefaultFromYamlChildrenNode(proxy, "obfs"); 242 | 243 | if (SimpleObfsPluginOptions.TryParseMode(simpleObfsModeString, out SimpleObfsPluginMode simpleObfsMode)) 244 | { 245 | server.PluginOptions = new SimpleObfsPluginOptions() 246 | { 247 | Mode = simpleObfsMode, 248 | Host = Yaml.GetStringOrDefaultFromYamlChildrenNode(proxy, "obfs-host") 249 | }; 250 | } 251 | else if (!string.IsNullOrWhiteSpace(simpleObfsModeString)) 252 | { 253 | this.logger.LogError($"Unsupported simple-obfs mode: {simpleObfsModeString}"); 254 | return null; 255 | } 256 | } 257 | 258 | return server; 259 | } 260 | } 261 | } -------------------------------------------------------------------------------- /ProxyStationService.Tests/HttpTrigger/FunctionsTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Http; 6 | using Microsoft.AspNetCore.Http.Internal; 7 | using Microsoft.AspNetCore.Mvc; 8 | using Microsoft.Extensions.Logging; 9 | using Microsoft.Extensions.Primitives; 10 | using NSubstitute; 11 | using ProxyStation.HttpTrigger; 12 | using ProxyStation.Util; 13 | using Xunit; 14 | using Xunit.Abstractions; 15 | 16 | namespace ProxyStation.Tests.HttpTrigger 17 | { 18 | public class FunctionsTests 19 | { 20 | private readonly ILogger logger; 21 | 22 | public FunctionsTests(ITestOutputHelper output) 23 | { 24 | this.logger = output.BuildLogger(); 25 | } 26 | 27 | [Fact] 28 | public async Task ShouldSuccessWithSurge() 29 | { 30 | var profileUrl = "test"; 31 | var profileName = "Test"; 32 | var profileConfig = $"{{\"source\": \"{profileUrl}\", \"type\": \"surge\", \"name\": \"{profileName}\"}}"; 33 | var profileContent = Fixtures.SurgeProfile1; 34 | 35 | var downloader = Substitute.For(); 36 | var environmentManager = Substitute.For(); 37 | var request = Substitute.For(); 38 | 39 | Functions.EnvironmentManager = environmentManager; 40 | Functions.Downloader = downloader; 41 | 42 | environmentManager.Get(profileName).Returns(profileConfig); 43 | downloader.Download(this.logger, profileUrl).Returns(profileContent); 44 | 45 | var result = await Functions.GetTrain(request, profileName, "surge-list", this.logger); 46 | Assert.IsType(result); 47 | 48 | var resultProfile = Encoding.UTF8.GetString((result as FileContentResult).FileContents); 49 | Assert.Equal(Fixtures.SurgeListProfile1, resultProfile); 50 | } 51 | 52 | [Fact] 53 | public async Task ShouldSuccessWithOriginal() 54 | { 55 | var profileUrl = "test"; 56 | var profileName = "Test"; 57 | var profileConfig = $"{{\"source\": \"{profileUrl}\", \"type\": \"surge\", \"name\": \"{profileName}\", \"allowDirectAccess\": true}}"; 58 | var profileContent = Fixtures.SurgeProfile1; 59 | 60 | var downloader = Substitute.For(); 61 | var environmentManager = Substitute.For(); 62 | var request = Substitute.For(); 63 | 64 | Functions.EnvironmentManager = environmentManager; 65 | Functions.Downloader = downloader; 66 | 67 | environmentManager.Get(profileName).Returns(profileConfig); 68 | downloader.Download(this.logger, profileUrl).Returns(profileContent); 69 | 70 | var result = await Functions.GetTrain(request, profileName, "original", this.logger); 71 | Assert.IsType(result); 72 | 73 | var resultProfile = Encoding.UTF8.GetString((result as FileContentResult).FileContents); 74 | Assert.Equal(profileContent, resultProfile); 75 | } 76 | 77 | [Fact] 78 | public async Task ShouldSuccessWithAliasType() 79 | { 80 | var profileUrl = "test"; 81 | var profileName = "TestProfile"; 82 | var profileConfig = $"{{\"source\": \"{profileUrl}\", \"type\": \"surge\", \"name\": \"{profileName}\"}}"; 83 | var profileContent = Fixtures.SurgeProfile2; 84 | 85 | var profileName1 = "TestProfileAlias1"; 86 | var profileConfig1 = $"{{\"source\": \"{profileName}\", \"type\": \"alias\", \"name\": \"{profileName1}\", \"filters\": [{{\"name\": \"name\", \"mode\": \"whitelist\", \"keyword\": \"香港\"}}]}}"; 87 | 88 | var profileName2 = "TestProfileAlias2"; 89 | var profileConfig2 = $"{{\"source\": \"{profileName1}\", \"type\": \"alias\", \"name\": \"{profileName2}\", \"filters\": [{{\"name\": \"name\", \"mode\": \"whitelist\", \"keyword\": \"中继\"}}]}}"; 90 | 91 | var profileName3 = "TestProfileAlias3"; 92 | var profileConfig3 = $"{{\"source\": \"{profileName2}\", \"type\": \"alias\", \"name\": \"{profileName3}\", \"filters\": [{{\"name\": \"name\", \"mode\": \"whitelist\", \"keyword\": \"高级\"}}]}}"; 93 | 94 | var downloader = Substitute.For(); 95 | var environmentManager = Substitute.For(); 96 | var request = Substitute.For(); 97 | 98 | Functions.EnvironmentManager = environmentManager; 99 | Functions.Downloader = downloader; 100 | 101 | environmentManager.Get(profileName).Returns(profileConfig); 102 | environmentManager.Get(profileName1).Returns(profileConfig1); 103 | environmentManager.Get(profileName2).Returns(profileConfig2); 104 | environmentManager.Get(profileName3).Returns(profileConfig3); 105 | downloader.Download(this.logger, profileUrl).Returns(profileContent); 106 | 107 | var result = await Functions.GetTrain(request, "test-profile-alias-3", "surge-list", this.logger); 108 | Assert.IsType(result); 109 | 110 | var resultProfile = Encoding.UTF8.GetString((result as FileContentResult).FileContents); 111 | Assert.Equal(Fixtures.SurgeListProfile2, resultProfile); 112 | } 113 | 114 | [Fact] 115 | public async Task ShouldReturn403IfCircularAliasReferenceIsDetected() 116 | { 117 | var profileName1 = "TestProfileAlias1"; 118 | var profileName2 = "TestProfileAlias2"; 119 | 120 | var profileConfig1 = $"{{\"source\": \"{profileName2}\", \"type\": \"alias\", \"name\": \"{profileName1}\"}}"; 121 | var profileConfig2 = $"{{\"source\": \"{profileName1}\", \"type\": \"alias\", \"name\": \"{profileName2}\"}}"; 122 | 123 | var downloader = Substitute.For(); 124 | var environmentManager = Substitute.For(); 125 | var request = Substitute.For(); 126 | 127 | Functions.EnvironmentManager = environmentManager; 128 | Functions.Downloader = downloader; 129 | 130 | environmentManager.Get(profileName1).Returns(profileConfig1); 131 | environmentManager.Get(profileName2).Returns(profileConfig2); 132 | 133 | var result = await Functions.GetTrain(request, "test-profile-alias-1", "surge-list", this.logger); 134 | Assert.IsType(result); 135 | } 136 | 137 | [Fact] 138 | public async Task ShouldFailBecauseOfInvalidTemplate() 139 | { 140 | var profileUrl = "test"; 141 | var profileName = "Test"; 142 | var profileConfig = $"{{\"source\": \"{profileUrl}\", \"type\": \"surge\", \"name\": \"{profileName}\"}}"; 143 | var profileContent = Fixtures.SurgeProfile1; 144 | 145 | var templateUrl = "https://template-url"; 146 | var templateContent = "asdafsdafsadf"; 147 | 148 | var downloader = Substitute.For(); 149 | var environmentManager = Substitute.For(); 150 | var request = Substitute.For(); 151 | 152 | Functions.EnvironmentManager = environmentManager; 153 | Functions.Downloader = downloader; 154 | 155 | request.Query.Returns(new QueryCollection(new Dictionary() { { "template", templateUrl } })); 156 | environmentManager.Get(profileName).Returns(profileConfig); 157 | downloader.Download(this.logger, profileUrl).Returns(profileContent); 158 | downloader.Download(this.logger, templateUrl).Returns(templateContent); 159 | 160 | var result = await Functions.GetTrain(request, profileName, "clash", this.logger); 161 | Assert.IsType(result); 162 | } 163 | 164 | [Fact] 165 | public async Task ShouldSuccessWithCustomClashTemplate() 166 | { 167 | var profileUrl = "test"; 168 | var profileName = "Test"; 169 | var profileConfig = $"{{\"source\": \"{profileUrl}\", \"type\": \"surge\", \"name\": \"{profileName}\"}}"; 170 | var profileContent = Fixtures.SurgeProfile1; 171 | 172 | var templateUrl = "https://template-url"; 173 | var templateContent = Fixtures.ClashTemplate1; 174 | 175 | var downloader = Substitute.For(); 176 | var environmentManager = Substitute.For(); 177 | var request = Substitute.For(); 178 | request.Scheme.Returns("https"); 179 | request.Host.Returns(new HostString("localhost")); 180 | request.Path.Returns(new PathString("/clash")); 181 | 182 | Functions.EnvironmentManager = environmentManager; 183 | Functions.Downloader = downloader; 184 | 185 | request.Query.Returns(new QueryCollection(new Dictionary() { { "template", templateUrl } })); 186 | environmentManager.Get(profileName).Returns(profileConfig); 187 | downloader.Download(this.logger, profileUrl).Returns(profileContent); 188 | downloader.Download(this.logger, templateUrl).Returns(templateContent); 189 | 190 | var result = await Functions.GetTrain(request, profileName, "clash", this.logger); 191 | Assert.IsType(result); 192 | 193 | var resultProfile = Encoding.UTF8.GetString((result as FileContentResult).FileContents); 194 | Assert.Equal(Fixtures.CustomClashProfile1, resultProfile); 195 | } 196 | 197 | [Fact] 198 | public async Task ShouldSuccessWithCustomSurgeTemplate() 199 | { 200 | var profileUrl = "test"; 201 | var profileName = "Test"; 202 | var profileConfig = $"{{\"source\": \"{profileUrl}\", \"type\": \"surge\", \"name\": \"{profileName}\"}}"; 203 | var profileContent = Fixtures.SurgeProfile1; 204 | 205 | var templateUrl = "https://template-url"; 206 | var templateContent = Fixtures.SurgeTemplate1; 207 | 208 | var downloader = Substitute.For(); 209 | var environmentManager = Substitute.For(); 210 | var request = Substitute.For(); 211 | 212 | Functions.EnvironmentManager = environmentManager; 213 | Functions.Downloader = downloader; 214 | 215 | request.Query.Returns(new QueryCollection(new Dictionary() { { "template", templateUrl } })); 216 | environmentManager.Get(profileName).Returns(profileConfig); 217 | downloader.Download(this.logger, profileUrl).Returns(profileContent); 218 | downloader.Download(this.logger, templateUrl).Returns(templateContent); 219 | 220 | var result = await Functions.GetTrain(request, profileName, "surge", this.logger); 221 | Assert.IsType(result); 222 | 223 | var resultProfile = Encoding.UTF8.GetString((result as FileContentResult).FileContents); 224 | Assert.Equal(Fixtures.CustomSurgeProfile1, resultProfile); 225 | } 226 | 227 | [Fact] 228 | public async Task ShouldSuccessWithCustomTemplateName() 229 | { 230 | var profileUrl = "test"; 231 | var profileName = "Test"; 232 | var profileConfig = $"{{\"source\": \"{profileUrl}\", \"type\": \"surge\", \"name\": \"{profileName}\"}}"; 233 | var profileContent = Fixtures.SurgeProfile1; 234 | 235 | var templateName = "Test1"; 236 | var templateUrl = "https://template-url"; 237 | var templateContent = Fixtures.SurgeTemplate1; 238 | 239 | var downloader = Substitute.For(); 240 | var environmentManager = Substitute.For(); 241 | var request = Substitute.For(); 242 | 243 | Functions.EnvironmentManager = environmentManager; 244 | Functions.Downloader = downloader; 245 | 246 | request.Query.Returns(new QueryCollection(new Dictionary() { { "template", templateName } })); 247 | environmentManager.Get(profileName).Returns(profileConfig); 248 | environmentManager.Get("Template" + templateName).Returns(templateUrl); 249 | downloader.Download(this.logger, profileUrl).Returns(profileContent); 250 | downloader.Download(this.logger, templateUrl).Returns(templateContent); 251 | 252 | var result = await Functions.GetTrain(request, profileName, "surge", this.logger); 253 | Assert.IsType(result); 254 | 255 | #pragma warning disable 4014 256 | downloader.Received().Download(this.logger, templateUrl); 257 | #pragma warning restore 4014 258 | } 259 | 260 | [Fact] 261 | public async Task ShouldSuccessWithQuantumultX() 262 | { 263 | var profileUrl = "test"; 264 | var profileName = "Test"; 265 | var profileConfig = $"{{\"source\": \"{profileUrl}\", \"type\": \"surge\", \"name\": \"{profileName}\"}}"; 266 | var profileContent = Fixtures.SurgeProfile2; 267 | 268 | var downloader = Substitute.For(); 269 | var environmentManager = Substitute.For(); 270 | var request = Substitute.For(); 271 | 272 | Functions.EnvironmentManager = environmentManager; 273 | Functions.Downloader = downloader; 274 | 275 | environmentManager.Get(profileName).Returns(profileConfig); 276 | downloader.Download(this.logger, profileUrl).Returns(profileContent); 277 | 278 | var result = await Functions.GetTrain(request, profileName, "quantumult-x", this.logger); 279 | Assert.IsType(result); 280 | 281 | var resultProfile = Encoding.UTF8.GetString((result as FileContentResult).FileContents); 282 | this.logger.LogInformation(resultProfile); 283 | // Assert.Equal(Fixtures.SurgeListProfile1, resultProfile); 284 | } 285 | 286 | [Fact] 287 | public async Task ShouldSuccessWithSurgeCorrectUrl() 288 | { 289 | var profileUrl = "test"; 290 | var profileName = "Test"; 291 | var profileConfig = $"{{\"source\": \"{profileUrl}\", \"type\": \"surge\", \"name\": \"{profileName}\"}}"; 292 | var profileContent = Fixtures.SurgeProfile1; 293 | 294 | var downloader = Substitute.For(); 295 | var environmentManager = Substitute.For(); 296 | var request = Substitute.For(); 297 | 298 | var queryString = "?helloworld=1"; 299 | request.QueryString.Returns(new QueryString(queryString)); 300 | 301 | Functions.EnvironmentManager = environmentManager; 302 | Functions.Downloader = downloader; 303 | 304 | environmentManager.Get(profileName).Returns(profileConfig); 305 | downloader.Download(this.logger, profileUrl).Returns(profileContent); 306 | 307 | var result = await Functions.GetTrain(request, profileName, "surge", this.logger); 308 | Assert.IsType(result); 309 | 310 | var resultProfile = Encoding.UTF8.GetString((result as FileContentResult).FileContents); 311 | Assert.Contains(queryString, resultProfile); 312 | } 313 | } 314 | } 315 | -------------------------------------------------------------------------------- /ProxyStationService.Tests/HttpTrigger/Fixtures.cs: -------------------------------------------------------------------------------- 1 | namespace ProxyStation.Tests.HttpTrigger 2 | { 3 | class Fixtures 4 | { 5 | public readonly static string SurgeProfile1 = @" 6 | [General] 7 | loglevel = notify 8 | bypass-system = true 9 | skip-proxy = 127.0.0.0/24,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,100.64.0.0/10,17.0.0.0/8,localhost,*.local,169.254.0.0/16,224.0.0.0/4,240.0.0.0/4 10 | bypass-tun = 127.0.0.0/24,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,100.64.0.0/10,17.0.0.0/8,localhost,*.local,169.254.0.0/16,224.0.0.0/4,240.0.0.0/4 11 | dns-server = system, 114.114.114.114, 119.29.29.29, 223.5.5.5, 8.8.8.8, 8.8.4.4 12 | replica = false 13 | enhanced-mode-by-rule = true 14 | ipv6 = false 15 | exclude-simple-hostnames = true 16 | test-timeout = 5 17 | allow-wifi-access = false 18 | 19 | [Replica] 20 | hide-apple-request=true 21 | 22 | [Proxy] 23 | sadfasd = ss, 12381293, 123, encrypt-method=aes-128-gcm, password=1231341, obfs=http, obfs-host=2341324124, udp-relay=true, interface=asdasd, allow-other-interface=true, tfo=true 24 | 香港高级 BGP 中继 5 = custom, hk5.edge.iplc.app, 155, rc4-md5, asdads, http://example.com/, udp-relay=true 25 | 🇭🇰 中国杭州 -> 香港 01 | IPLC = ss, 123.123.123.123, 10086, encrypt-method=xchacha20-ietf-poly1305, password=gasdas, obfs=tls, obfs-host=download.windowsupdate.com, udp-relay=true 26 | sadfasd=custom, 12381293, 123, encrypt-method=aes-128-gcm, password=1231341, obfs=http, obfs-host=2341324124, udp-relay=true, interface=asdasd, allow-other-interface=true, tfo=true 27 | 28 | [Proxy Group] 29 | AUTO = url-test, 香港高级 BGP 中继 1, 香港高级 BGP 中继 2, 香港高级 BGP 中继 3, 香港高级 BGP 中继 4, 香港高级 BGP 中继 5, 香港高级 BGP 中继 6, 香港高级 BGP 中继 7, 香港高级 BGP 中继 8, 香港高级 BGP 中继 9, 香港高级 BGP 中继 10, 香港高级 BGP 中继 11, 香港高级 BGP 中继 12, 香港高级 BGP 中继 13, 香港高级 BGP 中继 14, 香港高级 BGP 中继 15, 香港高级 BGP 中继 16, 香港 BGP 中继 1, 香港 BGP 中继 2, 香港 BGP 中继 3, 香港 BGP 中继 4, 香港 BGP 中继 5, 香港 BGP 中继 6, 香港 BGP 中继 7, 香港 BGP 中继 8, 香港 BGP 中继 9, 香港 BGP 中继 10, 香港 BGP 中继 11, 香港 BGP 中继 12, 香港 BGP 中继 13, 香港 BGP 中继 14, 香港 BGP 中继 15, 香港 BGP 中继 16, 香港标准 BGP 中继 1, 香港标准 BGP 中继 2, 香港标准 BGP 中继 3, 香港标准 BGP 中继 4, 香港标准 BGP 中继 5, url=http://www.gstatic.com/generate_204, interval=180, timeout=1, tolerance=100 30 | RIXCLOUD = select, AUTO, DIRECT, 香港高级 BGP 中继 1, 香港高级 BGP 中继 2, 香港高级 BGP 中继 3, 香港高级 BGP 中继 4, 香港高级 BGP 中继 5, 香港高级 BGP 中继 6, 香港高级 BGP 中继 7, 香港高级 BGP 中继 8, 香港高级 BGP 中继 9, 香港高级 BGP 中继 10, 香港高级 BGP 中继 11, 香港高级 BGP 中继 12, 香港高级 BGP 中继 13, 香港高级 BGP 中继 14, 香港高级 BGP 中继 15, 香港高级 BGP 中继 16, 香港 BGP 中继 1, 香港 BGP 中继 2, 香港 BGP 中继 3, 香港 BGP 中继 4, 香港 BGP 中继 5, 香港 BGP 中继 6, 香港 BGP 中继 7, 香港 BGP 中继 8, 香港 BGP 中继 9, 香港 BGP 中继 10, 香港 BGP 中继 11, 香港 BGP 中继 12, 香港 BGP 中继 13, 香港 BGP 中继 14, 香港 BGP 中继 15, 香港 BGP 中继 16, 香港标准 BGP 中继 1, 香港标准 BGP 中继 2, 香港标准 BGP 中继 3, 香港标准 BGP 中继 4, 香港标准 BGP 中继 5 31 | 32 | [Rule] 33 | GEOIP,CN,DIRECT 34 | FINAL,RIXCLOUD,dns-failed 35 | 36 | "; 37 | 38 | public readonly static string SurgeListProfile1 = @"sadfasd = ss, 12381293, 123, encrypt-method=aes-128-gcm, password=1231341, obfs=http, obfs-host=2341324124, udp-relay=true, tfo=true 39 | 香港高级 BGP 中继 5 = ss, hk5.edge.iplc.app, 155, encrypt-method=rc4-md5, password=asdads, udp-relay=true 40 | 🇭🇰 中国杭州 -> 香港 01 | IPLC = ss, 123.123.123.123, 10086, encrypt-method=xchacha20-ietf-poly1305, password=gasdas, obfs=tls, obfs-host=download.windowsupdate.com, udp-relay=true 41 | sadfasd = ss, 12381293, 123, encrypt-method=aes-128-gcm, password=1231341, obfs=http, obfs-host=2341324124, udp-relay=true, tfo=true 42 | "; 43 | 44 | public readonly static string ClashTemplate1 = @"port: 7890 45 | socks-port: 7891 46 | allow-lan: true 47 | bind-address: '*' 48 | mode: Rule 49 | log-level: info 50 | external-controller: 127.0.0.1:9090 51 | 52 | proxy-groups: 53 | - name: Proxy 54 | type: url-test 55 | url: http://www.gstatic.com/generate_204 56 | interval: 300 57 | - name: Default 58 | type: select 59 | proxies: 60 | - Proxy 61 | - DIRECT 62 | - name: AdBlock 63 | type: select 64 | proxies: 65 | - REJECT 66 | - DIRECT 67 | - Proxy 68 | 69 | proxy-provider: 70 | Proxy Endpoints: 71 | type: http 72 | path: ./endpoints.yaml 73 | slot: true 74 | interval: 3600 75 | health-check: 76 | enable: true 77 | url: http://www.gstatic.com/generate_204 78 | interval: 300 79 | 80 | Rule: 81 | - GEOIP,CN,DIRECT 82 | - MATCH,Default 83 | - FINAL,Default 84 | "; 85 | 86 | public readonly static string CustomClashProfile1 = @"port: 7890 87 | socks-port: 7891 88 | allow-lan: true 89 | bind-address: '*' 90 | mode: Rule 91 | log-level: info 92 | external-controller: 127.0.0.1:9090 93 | proxy-groups: 94 | - name: Proxy 95 | type: url-test 96 | url: http://www.gstatic.com/generate_204 97 | interval: 300 98 | - name: Default 99 | type: select 100 | proxies: 101 | - Proxy 102 | - DIRECT 103 | - name: AdBlock 104 | type: select 105 | proxies: 106 | - REJECT 107 | - DIRECT 108 | - Proxy 109 | proxy-provider: 110 | Proxy Endpoints: 111 | type: http 112 | path: ./endpoints.yaml 113 | url: https://localhost/clash-proxy-provider 114 | interval: 3600 115 | health-check: 116 | enable: true 117 | url: http://www.gstatic.com/generate_204 118 | interval: 300 119 | Rule: 120 | - GEOIP,CN,DIRECT 121 | - MATCH,Default 122 | - FINAL,Default 123 | "; 124 | 125 | public readonly static string SurgeTemplate1 = @"[General] 126 | interface = 0.0.0.0 127 | socks-interface = 0.0.0.0 128 | port = 8888 129 | socks-port = 8889 130 | 131 | [Proxy] 132 | {% SERVER_LIST %} 133 | 134 | [Proxy Group] 135 | Default = select, Proxy, DIRECT 136 | Proxy = url-test, {% SERVER_NAMES %}, url=http://captive.apple.com, interval=600, tolerance=200 137 | 138 | [Rule] 139 | RULE-SET,LAN,DIRECT 140 | GEOIP,CN,DIRECT 141 | FINAL,Default,dns-failed 142 | "; 143 | 144 | public readonly static string CustomSurgeProfile1 = @"#!MANAGED-CONFIG ://?template=https%3a%2f%2ftemplate-url interval=43200 145 | [General] 146 | interface = 0.0.0.0 147 | socks-interface = 0.0.0.0 148 | port = 8888 149 | socks-port = 8889 150 | 151 | [Proxy] 152 | sadfasd = ss, 12381293, 123, encrypt-method=aes-128-gcm, password=1231341, obfs=http, obfs-host=2341324124, udp-relay=true, tfo=true 153 | 香港高级 BGP 中继 5 = ss, hk5.edge.iplc.app, 155, encrypt-method=rc4-md5, password=asdads, udp-relay=true 154 | 🇭🇰 中国杭州 -> 香港 01 | IPLC = ss, 123.123.123.123, 10086, encrypt-method=xchacha20-ietf-poly1305, password=gasdas, obfs=tls, obfs-host=download.windowsupdate.com, udp-relay=true 155 | sadfasd = ss, 12381293, 123, encrypt-method=aes-128-gcm, password=1231341, obfs=http, obfs-host=2341324124, udp-relay=true, tfo=true 156 | 157 | 158 | [Proxy Group] 159 | Default = select, Proxy, DIRECT 160 | Proxy = url-test, sadfasd, 香港高级 BGP 中继 5, 🇭🇰 中国杭州 -> 香港 01 | IPLC, sadfasd, url=http://captive.apple.com, interval=600, tolerance=200 161 | 162 | [Rule] 163 | RULE-SET,LAN,DIRECT 164 | GEOIP,CN,DIRECT 165 | FINAL,Default,dns-failed 166 | "; 167 | 168 | 169 | public readonly static string SurgeProfile2 = @" 170 | [General] 171 | loglevel = notify 172 | bypass-system = true 173 | skip-proxy = 127.0.0.0/24,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,100.64.0.0/10,17.0.0.0/8,localhost,*.local,169.254.0.0/16,224.0.0.0/4,240.0.0.0/4 174 | bypass-tun = 127.0.0.0/24,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,100.64.0.0/10,17.0.0.0/8,localhost,*.local,169.254.0.0/16,224.0.0.0/4,240.0.0.0/4 175 | dns-server = system, 114.114.114.114, 119.29.29.29, 223.5.5.5, 8.8.8.8, 8.8.4.4 176 | replica = false 177 | enhanced-mode-by-rule = true 178 | ipv6 = false 179 | exclude-simple-hostnames = true 180 | test-timeout = 5 181 | allow-wifi-access = false 182 | 183 | [Replica] 184 | hide-apple-request=true 185 | 186 | [Proxy] 187 | 香港 BGP 1 = ss, 123.123.123.123, 10086, encrypt-method=xchacha20-ietf-poly1305, password=gasdas, obfs=tls, obfs-host=download.windowsupdate.com, udp-relay=true 188 | 香港 BGP 2 = ss, 123.123.123.123, 10086, encrypt-method=xchacha20-ietf-poly1305, password=gasdas, obfs=tls, obfs-host=download.windowsupdate.com, udp-relay=true 189 | 香港 BGP 中继 1 = ss, 123.123.123.123, 10086, encrypt-method=xchacha20-ietf-poly1305, password=gasdas, obfs=tls, obfs-host=download.windowsupdate.com, udp-relay=true 190 | 香港 BGP 中继 2 = ss, 123.123.123.123, 10086, encrypt-method=xchacha20-ietf-poly1305, password=gasdas, obfs=tls, obfs-host=download.windowsupdate.com, udp-relay=true 191 | 香港高级 BGP 中继 1 = ss, 123.123.123.123, 10086, encrypt-method=xchacha20-ietf-poly1305, password=gasdas, obfs=tls, obfs-host=download.windowsupdate.com, udp-relay=true 192 | 香港高级 BGP 中继 2 = ss, 123.123.123.123, 10086, encrypt-method=xchacha20-ietf-poly1305, password=gasdas, obfs=tls, obfs-host=download.windowsupdate.com, udp-relay=true 193 | 日本 BGP 1 = ss, 123.123.123.123, 10086, encrypt-method=xchacha20-ietf-poly1305, password=gasdas, obfs=tls, obfs-host=download.windowsupdate.com, udp-relay=true 194 | 日本 BGP 2 = ss, 123.123.123.123, 10086, encrypt-method=xchacha20-ietf-poly1305, password=gasdas, obfs=tls, obfs-host=download.windowsupdate.com, udp-relay=true 195 | 日本 BGP 中继 1 = ss, 123.123.123.123, 10086, encrypt-method=xchacha20-ietf-poly1305, password=gasdas, obfs=tls, obfs-host=download.windowsupdate.com, udp-relay=true 196 | 日本 BGP 中继 2 = ss, 123.123.123.123, 10086, encrypt-method=xchacha20-ietf-poly1305, password=gasdas, obfs=tls, obfs-host=download.windowsupdate.com, udp-relay=true 197 | 日本高级 BGP 中继 1 = ss, 123.123.123.123, 10086, encrypt-method=xchacha20-ietf-poly1305, password=gasdas, obfs=tls, obfs-host=download.windowsupdate.com, udp-relay=true 198 | 日本高级 BGP 中继 2 = ss, 123.123.123.123, 10086, encrypt-method=xchacha20-ietf-poly1305, password=gasdas, obfs=tls, obfs-host=download.windowsupdate.com, udp-relay=true 199 | 200 | [Proxy Group] 201 | AUTO = url-test, 香港高级 BGP 中继 1, 香港高级 BGP 中继 2, 香港高级 BGP 中继 3, 香港高级 BGP 中继 4, 香港高级 BGP 中继 5, 香港高级 BGP 中继 6, 香港高级 BGP 中继 7, 香港高级 BGP 中继 8, 香港高级 BGP 中继 9, 香港高级 BGP 中继 10, 香港高级 BGP 中继 11, 香港高级 BGP 中继 12, 香港高级 BGP 中继 13, 香港高级 BGP 中继 14, 香港高级 BGP 中继 15, 香港高级 BGP 中继 16, 香港 BGP 中继 1, 香港 BGP 中继 2, 香港 BGP 中继 3, 香港 BGP 中继 4, 香港 BGP 中继 5, 香港 BGP 中继 6, 香港 BGP 中继 7, 香港 BGP 中继 8, 香港 BGP 中继 9, 香港 BGP 中继 10, 香港 BGP 中继 11, 香港 BGP 中继 12, 香港 BGP 中继 13, 香港 BGP 中继 14, 香港 BGP 中继 15, 香港 BGP 中继 16, 香港标准 BGP 中继 1, 香港标准 BGP 中继 2, 香港标准 BGP 中继 3, 香港标准 BGP 中继 4, 香港标准 BGP 中继 5, url=http://www.gstatic.com/generate_204, interval=180, timeout=1, tolerance=100 202 | RIXCLOUD = select, AUTO, DIRECT, 香港高级 BGP 中继 1, 香港高级 BGP 中继 2, 香港高级 BGP 中继 3, 香港高级 BGP 中继 4, 香港高级 BGP 中继 5, 香港高级 BGP 中继 6, 香港高级 BGP 中继 7, 香港高级 BGP 中继 8, 香港高级 BGP 中继 9, 香港高级 BGP 中继 10, 香港高级 BGP 中继 11, 香港高级 BGP 中继 12, 香港高级 BGP 中继 13, 香港高级 BGP 中继 14, 香港高级 BGP 中继 15, 香港高级 BGP 中继 16, 香港 BGP 中继 1, 香港 BGP 中继 2, 香港 BGP 中继 3, 香港 BGP 中继 4, 香港 BGP 中继 5, 香港 BGP 中继 6, 香港 BGP 中继 7, 香港 BGP 中继 8, 香港 BGP 中继 9, 香港 BGP 中继 10, 香港 BGP 中继 11, 香港 BGP 中继 12, 香港 BGP 中继 13, 香港 BGP 中继 14, 香港 BGP 中继 15, 香港 BGP 中继 16, 香港标准 BGP 中继 1, 香港标准 BGP 中继 2, 香港标准 BGP 中继 3, 香港标准 BGP 中继 4, 香港标准 BGP 中继 5 203 | 204 | [Rule] 205 | GEOIP,CN,DIRECT 206 | FINAL,RIXCLOUD,dns-failed 207 | 208 | "; 209 | 210 | public readonly static string SurgeListProfile2 = @"香港高级 BGP 中继 1 = ss, 123.123.123.123, 10086, encrypt-method=xchacha20-ietf-poly1305, password=gasdas, obfs=tls, obfs-host=download.windowsupdate.com, udp-relay=true 211 | 香港高级 BGP 中继 2 = ss, 123.123.123.123, 10086, encrypt-method=xchacha20-ietf-poly1305, password=gasdas, obfs=tls, obfs-host=download.windowsupdate.com, udp-relay=true 212 | "; 213 | 214 | public readonly static string QuantumultXProfile2 = @"[general] 215 | excluded_routes = 192.168.0.0/16, 10.0.0.0/8, 172.16.0.0/12, 100.64.0.0/10, 17.0.0.0/8 216 | network_check_url = http://www.gstatic.com/generate_204 217 | server_check_url = http://www.gstatic.cn/generate_204 218 | geo_location_checker = http://ip-api.com/json/?lang=zh-CN, https://github.com/KOP-XIAO/QuantumultX/raw/master/Scripts/IP_API.js 219 | 220 | [dns] 221 | server = 119.29.29.29 222 | server = 119.28.28.28 223 | server = 1.2.4.8 224 | server = 182.254.116.116 225 | 226 | [policy] 227 | static=ADBlock, direct, GlobalTraffic, reject 228 | static=CNTraffic, direct, GlobalTraffic 229 | available=Auto, 香港 BGP 1, 香港 BGP 2, 香港 BGP 中继 1, 香港 BGP 中继 2, 香港高级 BGP 中继 1, 香港高级 BGP 中继 2, 日本 BGP 1, 日本 BGP 2, 日本 BGP 中继 1, 日本 BGP 中继 2, 日本高级 BGP 中继 1, 日本高级 BGP 中继 2 230 | static=GlobalTraffic, Auto, 香港 BGP 1, 香港 BGP 2, 香港 BGP 中继 1, 香港 BGP 中继 2, 香港高级 BGP 中继 1, 香港高级 BGP 中继 2, 日本 BGP 1, 日本 BGP 2, 日本 BGP 中继 1, 日本 BGP 中继 2, 日本高级 BGP 中继 1, 日本高级 BGP 中继 2, direct 231 | 232 | [server_local] 233 | shadowsocks=123.123.123.123:10086, method=xchacha20-ietf-poly1305, password=gasdas, obfs=tls, obfs-host=download.windowsupdate.com, fast-open=false, udp-relay=true, tag=香港 BGP 1 234 | shadowsocks=123.123.123.123:10086, method=xchacha20-ietf-poly1305, password=gasdas, obfs=tls, obfs-host=download.windowsupdate.com, fast-open=false, udp-relay=true, tag=香港 BGP 2 235 | shadowsocks=123.123.123.123:10086, method=xchacha20-ietf-poly1305, password=gasdas, obfs=tls, obfs-host=download.windowsupdate.com, fast-open=false, udp-relay=true, tag=香港 BGP 中继 1 236 | shadowsocks=123.123.123.123:10086, method=xchacha20-ietf-poly1305, password=gasdas, obfs=tls, obfs-host=download.windowsupdate.com, fast-open=false, udp-relay=true, tag=香港 BGP 中继 2 237 | shadowsocks=123.123.123.123:10086, method=xchacha20-ietf-poly1305, password=gasdas, obfs=tls, obfs-host=download.windowsupdate.com, fast-open=false, udp-relay=true, tag=香港高级 BGP 中继 1 238 | shadowsocks=123.123.123.123:10086, method=xchacha20-ietf-poly1305, password=gasdas, obfs=tls, obfs-host=download.windowsupdate.com, fast-open=false, udp-relay=true, tag=香港高级 BGP 中继 2 239 | shadowsocks=123.123.123.123:10086, method=xchacha20-ietf-poly1305, password=gasdas, obfs=tls, obfs-host=download.windowsupdate.com, fast-open=false, udp-relay=true, tag=日本 BGP 1 240 | shadowsocks=123.123.123.123:10086, method=xchacha20-ietf-poly1305, password=gasdas, obfs=tls, obfs-host=download.windowsupdate.com, fast-open=false, udp-relay=true, tag=日本 BGP 2 241 | shadowsocks=123.123.123.123:10086, method=xchacha20-ietf-poly1305, password=gasdas, obfs=tls, obfs-host=download.windowsupdate.com, fast-open=false, udp-relay=true, tag=日本 BGP 中继 1 242 | shadowsocks=123.123.123.123:10086, method=xchacha20-ietf-poly1305, password=gasdas, obfs=tls, obfs-host=download.windowsupdate.com, fast-open=false, udp-relay=true, tag=日本 BGP 中继 2 243 | shadowsocks=123.123.123.123:10086, method=xchacha20-ietf-poly1305, password=gasdas, obfs=tls, obfs-host=download.windowsupdate.com, fast-open=false, udp-relay=true, tag=日本高级 BGP 中继 1 244 | shadowsocks=123.123.123.123:10086, method=xchacha20-ietf-poly1305, password=gasdas, obfs=tls, obfs-host=download.windowsupdate.com, fast-open=false, udp-relay=true, tag=日本高级 BGP 中继 2 245 | 246 | [filter_remote] 247 | https://raw.githubusercontent.com/GeQ1an/Rules/master/QuantumultX/Filter/AdBlock.list, tag=AdBlock, force-policy=GlobalTraffic, 248 | https://raw.githubusercontent.com/GeQ1an/Rules/master/QuantumultX/Filter/Netease%20Music.list, tag=Netease Music, force-policy=direct, 249 | https://raw.githubusercontent.com/GeQ1an/Rules/master/QuantumultX/Filter/Netflix.list, tag=Netflix, force-policy=GlobalTraffic, 250 | https://raw.githubusercontent.com/GeQ1an/Rules/master/QuantumultX/Filter/YouTube.list, tag=YouTube, force-policy=GlobalTraffic, 251 | https://raw.githubusercontent.com/GeQ1an/Rules/master/QuantumultX/Filter/Microsoft.list, tag=Microsoft, force-policy=GlobalTraffic, 252 | https://raw.githubusercontent.com/GeQ1an/Rules/master/QuantumultX/Filter/CMedia.list, tag=CMedia, force-policy=direct, 253 | https://raw.githubusercontent.com/GeQ1an/Rules/master/QuantumultX/Filter/GMedia.list, tag=GMedia, force-policy=GlobalTraffic, 254 | https://raw.githubusercontent.com/GeQ1an/Rules/master/QuantumultX/Filter/PayPal.list, tag=PayPal, force-policy=GlobalTraffic, 255 | https://raw.githubusercontent.com/GeQ1an/Rules/master/QuantumultX/Filter/Speedtest.list, tag=Speedtest, force-policy=GlobalTraffic, 256 | https://raw.githubusercontent.com/GeQ1an/Rules/master/QuantumultX/Filter/Telegram.list, tag=Telegram, force-policy=GlobalTraffic, 257 | https://raw.githubusercontent.com/GeQ1an/Rules/master/QuantumultX/Filter/Outside.list, tag=Outside, force-policy=GlobalTraffic, 258 | https://raw.githubusercontent.com/GeQ1an/Rules/master/QuantumultX/Filter/Mainland.list, tag=Mainland, force-policy=direct, 259 | https://raw.githubusercontent.com/GeQ1an/Rules/master/QuantumultX/Filter/Apple.list, tag=Apple, force-policy=GlobalTraffic, 260 | 261 | [filter_local] 262 | host-suffix, local, direct 263 | ip-cidr, 10.0.0.0/8, direct 264 | ip-cidr, 17.0.0.0/8, direct 265 | ip-cidr, 100.64.0.0/10, direct 266 | ip-cidr, 127.0.0.0/8, direct 267 | ip-cidr, 172.16.0.0/12, direct 268 | ip-cidr, 192.168.0.0/16, direct 269 | geoip, cn, direct 270 | final, GlobalTraffic 271 | 272 | [rewrite_remote] 273 | https://raw.githubusercontent.com/ConnersHua/Profiles/master/Quantumult/X/Rewrite.conf, tag=Rewrite (ConnersHua), enabled=true 274 | http://cloudcompute.lbyczf.com/quanx-rewrite, tag=Rewrite (lhie1), enabled=false 275 | "; 276 | } 277 | } -------------------------------------------------------------------------------- /ProxyStationService/Parser/SurgeParser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Text.RegularExpressions; 6 | using Microsoft.Extensions.Logging; 7 | using ProxyStation.Model; 8 | using ProxyStation.ProfileParser.Template; 9 | using ProxyStation.Util; 10 | 11 | namespace ProxyStation.ProfileParser 12 | { 13 | public class SurgeEncodeOptions : EncodeOptions 14 | { 15 | public string ProfileURL { get; set; } 16 | } 17 | 18 | public class SurgeParser : IProfileParser 19 | { 20 | 21 | private ILogger logger; 22 | 23 | public SurgeParser(ILogger logger) 24 | { 25 | this.logger = logger; 26 | } 27 | 28 | public bool ValidateTemplate(string template) 29 | { 30 | if (string.IsNullOrEmpty(template)) 31 | { 32 | return true; 33 | } 34 | 35 | return template.Contains(Surge.ServerListPlaceholder) && template.Contains(Surge.ServerNamesPlaceholder); 36 | } 37 | 38 | public Server ParseSnellServer(string[] properties) 39 | { 40 | // snell, [SERVER ADDRESS], [GENERATED PORT], psk=[GENERATED PSK], obfs=http 41 | if (properties.Length < 3) 42 | { 43 | this.logger.LogError("Invaild surge shadowsocks proxy: " + string.Join(", ", properties)); 44 | return null; 45 | } 46 | 47 | if (!int.TryParse(properties[2].Trim(), out int port)) 48 | { 49 | this.logger.LogError("Invalid snell port: ", properties[2].Trim()); 50 | return null; 51 | }; 52 | 53 | 54 | var server = new SnellServer() 55 | { 56 | Host = properties[1].Trim(), 57 | Port = port, 58 | }; 59 | 60 | var keyValues = properties 61 | .Skip(3) 62 | .Select(p => p.Trim().Split("=", 2)) 63 | .Where(p => p.Length == 2) 64 | .Select(p => new string[] { p[0].TrimEnd(), p[1].TrimStart() }); 65 | 66 | string obfs = null, obfsHost = null; 67 | foreach (var keyValue in keyValues) 68 | { 69 | var key = keyValue[0]; 70 | if (key == "psk" || key == "password") 71 | { 72 | server.Password = keyValue[1]; 73 | } 74 | else if (key == "obfs") 75 | { 76 | obfs = keyValue[1]; 77 | } 78 | else if (key == "obfs-host") 79 | { 80 | obfsHost = keyValue[1]; 81 | } 82 | else if (key == "tfo") 83 | { 84 | server.FastOpen = keyValue[1] == "true"; 85 | } 86 | else if (key != "interface" && key != "allow-other-interface") 87 | { 88 | this.logger.LogWarning($"Unrecognized snell options {key}={keyValue[1]}"); 89 | } 90 | } 91 | 92 | if (server.Password == null) 93 | { 94 | this.logger.LogError("Snell password is missing!"); 95 | return null; 96 | } 97 | 98 | var obfsMode = default(SnellObfuscationMode); 99 | switch (obfs) 100 | { 101 | case "http": 102 | obfsMode = SnellObfuscationMode.HTTP; 103 | break; 104 | case "tls": 105 | obfsMode = SnellObfuscationMode.TLS; 106 | break; 107 | case null: 108 | obfsMode = SnellObfuscationMode.None; 109 | obfsHost = null; 110 | break; 111 | default: 112 | this.logger.LogError("Unknown snell obfusaction mode: " + obfs); 113 | return null; 114 | } 115 | 116 | server.ObfuscationMode = obfsMode; 117 | server.ObfuscationHost = obfsHost; 118 | return server; 119 | } 120 | 121 | public Server ParseClassicShadowsocksServer(string[] properties) 122 | { 123 | if (properties.Length < 5) 124 | { 125 | this.logger.LogError("Invaild surge shadowsocks proxy: " + string.Join(", ", properties)); 126 | return null; 127 | } 128 | 129 | if (!int.TryParse(properties[2].Trim(), out int port)) 130 | { 131 | this.logger.LogError("Invalid shadowsocks port: ", properties[2].Trim()); 132 | return null; 133 | }; 134 | 135 | 136 | var server = new ShadowsocksServer() 137 | { 138 | Host = properties[1].Trim(), 139 | Port = port, 140 | Method = properties[3].Trim(), 141 | Password = properties[4].Trim(), 142 | }; 143 | 144 | var keyValues = properties 145 | .Skip(3) 146 | .Select(p => p.Trim().Split("=", 2)) 147 | .Where(p => p.Length == 2) 148 | .Select(p => new string[] { p[0].TrimEnd(), p[1].TrimStart() }); 149 | 150 | string obfs = null, obfsHost = null; 151 | foreach (var keyValue in keyValues) 152 | { 153 | var key = keyValue[0]; 154 | if (key == "psk" || key == "password") 155 | { 156 | server.Password = keyValue[1]; 157 | } 158 | else if (key == "encrypt-method") 159 | { 160 | server.Method = keyValue[1]; 161 | } 162 | else if (key == "obfs") 163 | { 164 | obfs = keyValue[1]; 165 | } 166 | else if (key == "obfs-host") 167 | { 168 | obfsHost = keyValue[1]; 169 | } 170 | else if (key == "tfo") 171 | { 172 | server.FastOpen = keyValue[1] == "true"; 173 | } 174 | else if (key == "udp-relay") 175 | { 176 | server.UDPRelay = keyValue[1] == "true"; 177 | } 178 | else if (key != "interface" && key != "allow-other-interface") 179 | { 180 | this.logger.LogWarning($"Unrecognized shadowsocks options {key}={keyValue[1]}"); 181 | } 182 | } 183 | 184 | if (server.Password == null) 185 | { 186 | this.logger.LogError("Shadowsocks password is missing!"); 187 | return null; 188 | } 189 | 190 | if (server.Method == null) 191 | { 192 | this.logger.LogError("Shadowsocks method is missing!"); 193 | return null; 194 | } 195 | 196 | switch (obfs) 197 | { 198 | case "http": 199 | server.PluginOptions = new SimpleObfsPluginOptions() 200 | { 201 | Mode = SimpleObfsPluginMode.HTTP, 202 | Host = obfsHost, 203 | }; 204 | break; 205 | case "tls": 206 | server.PluginOptions = new SimpleObfsPluginOptions() 207 | { 208 | Mode = SimpleObfsPluginMode.TLS, 209 | Host = obfsHost, 210 | }; 211 | break; 212 | case null: 213 | break; 214 | default: 215 | this.logger.LogError("Unknown shadowsocks obfusaction mode: " + obfs); 216 | return null; 217 | } 218 | 219 | return server; 220 | } 221 | 222 | public Server ParseShadowsocksServer(string[] properties) 223 | { 224 | if (properties.Length < 3) 225 | { 226 | this.logger.LogError("Invaild surge shadowsocks proxy: " + string.Join(", ", properties)); 227 | return null; 228 | } 229 | 230 | if (!int.TryParse(properties[2].Trim(), out int port)) 231 | { 232 | this.logger.LogError("Invalid shadowsocks port: ", properties[2].Trim()); 233 | return null; 234 | }; 235 | 236 | 237 | var server = new ShadowsocksServer() 238 | { 239 | Host = properties[1].Trim(), 240 | Port = port, 241 | }; 242 | 243 | var keyValues = properties 244 | .Skip(3) 245 | .Select(p => p.Trim().Split("=", 2)) 246 | .Where(p => p.Length == 2) 247 | .Select(p => new string[] { p[0].TrimEnd(), p[1].TrimStart() }); 248 | 249 | string obfs = null, obfsHost = null; 250 | foreach (var keyValue in keyValues) 251 | { 252 | var key = keyValue[0]; 253 | if (key == "psk" || key == "password") 254 | { 255 | server.Password = keyValue[1]; 256 | } 257 | else if (key == "encrypt-method") 258 | { 259 | server.Method = keyValue[1]; 260 | } 261 | else if (key == "obfs") 262 | { 263 | obfs = keyValue[1]; 264 | } 265 | else if (key == "obfs-host") 266 | { 267 | obfsHost = keyValue[1]; 268 | } 269 | else if (key == "tfo") 270 | { 271 | server.FastOpen = keyValue[1] == "true"; 272 | } 273 | else if (key == "udp-relay") 274 | { 275 | server.UDPRelay = keyValue[1] == "true"; 276 | } 277 | else if (key != "interface" && key != "allow-other-interface") 278 | { 279 | this.logger.LogWarning($"Unrecognized shadowsocks options {key}={keyValue[1]}"); 280 | } 281 | } 282 | 283 | if (server.Password == null) 284 | { 285 | this.logger.LogError("Shadowsocks password is missing!"); 286 | return null; 287 | } 288 | 289 | if (server.Method == null) 290 | { 291 | this.logger.LogError("Shadowsocks method is missing!"); 292 | return null; 293 | } 294 | 295 | switch (obfs) 296 | { 297 | case "http": 298 | server.PluginOptions = new SimpleObfsPluginOptions() 299 | { 300 | Mode = SimpleObfsPluginMode.HTTP, 301 | Host = obfsHost, 302 | }; 303 | break; 304 | case "tls": 305 | server.PluginOptions = new SimpleObfsPluginOptions() 306 | { 307 | Mode = SimpleObfsPluginMode.TLS, 308 | Host = obfsHost, 309 | }; 310 | break; 311 | case null: 312 | break; 313 | default: 314 | this.logger.LogError("Unknown shadowsocks obfusaction mode: " + obfs); 315 | return null; 316 | } 317 | 318 | 319 | return server; 320 | } 321 | 322 | public Server[] ParseProxyList(string profile) 323 | => string.IsNullOrWhiteSpace(profile) ? null : this.ParseProxyList(profile.Trim().Split("\n")); 324 | 325 | public Server[] ParseProxyList(IEnumerable plainProxies) 326 | { 327 | var servers = new List(); 328 | 329 | foreach (var proxy in plainProxies) 330 | { 331 | var parts = proxy.Trim().Split("=", 2); 332 | if (parts.Length != 2) 333 | { 334 | this.logger.LogError("Ignore invalid surge proxy line: " + proxy); 335 | } 336 | 337 | var name = parts[0]; 338 | var properties = parts[1].Split(","); 339 | 340 | Server server = properties[0].Trim() switch 341 | { 342 | "ss" => this.ParseShadowsocksServer(properties), 343 | "custom" => this.ParseClassicShadowsocksServer(properties), 344 | "snell" => this.ParseSnellServer(properties), 345 | _ => null, 346 | }; 347 | 348 | if (server == null) 349 | { 350 | this.logger.LogError("Ignore unsupported protocol: " + properties[0]); 351 | } 352 | else 353 | { 354 | server.Name = name.TrimEnd(); 355 | servers.Add(server); 356 | } 357 | } 358 | return servers.ToArray(); 359 | } 360 | 361 | public Server[] Parse(string profile) 362 | { 363 | var properties = Misc.ParsePropertieFile(profile); 364 | 365 | var plainProxies = properties.GetValueOrDefault("Proxy"); 366 | if (plainProxies == null) 367 | { 368 | return null; 369 | } 370 | 371 | return ParseProxyList(plainProxies); 372 | } 373 | 374 | public string Encode(EncodeOptions options, Server[] servers, out Server[] encodedServers) 375 | { 376 | if (!this.ValidateTemplate(options.Template)) 377 | { 378 | throw new InvalidTemplateException(); 379 | } 380 | 381 | var template = string.IsNullOrEmpty(options.Template) ? Surge.Template : options.Template; 382 | 383 | if (options is SurgeEncodeOptions) 384 | { 385 | var surgeOptions = options as SurgeEncodeOptions; 386 | if (!string.IsNullOrEmpty(surgeOptions.ProfileURL)) 387 | { 388 | template = $"#!MANAGED-CONFIG {surgeOptions.ProfileURL} interval=43200\r\n" + template; 389 | } 390 | } 391 | 392 | return template 393 | .Replace(Surge.ServerListPlaceholder, EncodeProxyList(servers, out encodedServers)) 394 | .Replace(Surge.ServerNamesPlaceholder, string.Join(", ", servers.Select(s => s.Name))); 395 | } 396 | 397 | public string EncodeProxyList(Server[] servers, out Server[] encodedServers) 398 | { 399 | var encodedServerList = new List(); 400 | var sb = new StringBuilder(); 401 | foreach (var server in servers) 402 | { 403 | if (server is ShadowsocksServer ssServer) 404 | { 405 | sb.Append($"{server.Name} = ss, {server.Host}, {server.Port}, encrypt-method={ssServer.Method}, password={ssServer.Password}"); 406 | 407 | if (ssServer.PluginOptions is SimpleObfsPluginOptions options) 408 | { 409 | var obfsHost = string.IsNullOrEmpty(options.Host) ? Constant.ObfsucationHost : options.Host; 410 | sb.Append($", obfs={SurgeParser.FormatSimpleObfsPluginMode(options.Mode)}, obfs-host={obfsHost}"); 411 | } 412 | 413 | if (ssServer.UDPRelay) 414 | { 415 | sb.Append(", udp-relay=true"); 416 | } 417 | 418 | if (ssServer.FastOpen) 419 | { 420 | sb.Append(", tfo=true"); 421 | } 422 | 423 | sb.AppendLine(); 424 | encodedServerList.Add(server); 425 | } 426 | else if (server is SnellServer snellServer) 427 | { 428 | sb.Append($"{server.Name} = ss, {server.Host}, {server.Port}, psk={snellServer.Password}"); 429 | 430 | if (snellServer.ObfuscationMode != SnellObfuscationMode.None) 431 | { 432 | sb.Append(", obfs=" + snellServer.ObfuscationMode.ToString().ToLower()); 433 | sb.Append(", obfs-host=" + (string.IsNullOrWhiteSpace(snellServer.ObfuscationHost) ? Constant.ObfsucationHost : snellServer.ObfuscationHost)); 434 | } 435 | 436 | if (snellServer.FastOpen) 437 | { 438 | sb.Append(", tfo=true"); 439 | } 440 | 441 | sb.AppendLine(); 442 | encodedServerList.Add(server); 443 | } 444 | else 445 | { 446 | this.logger.LogInformation($"Server {server} is ignored."); 447 | } 448 | } 449 | 450 | encodedServers = encodedServerList.ToArray(); 451 | return sb.ToString(); 452 | } 453 | 454 | public string ExtName() => ""; 455 | 456 | static string FormatSimpleObfsPluginMode(SimpleObfsPluginMode mode) 457 | { 458 | return mode switch 459 | { 460 | SimpleObfsPluginMode.HTTP => "http", 461 | SimpleObfsPluginMode.TLS => "tls", 462 | _ => throw new NotImplementedException(), 463 | }; 464 | } 465 | } 466 | } -------------------------------------------------------------------------------- /ProxyStationService/Parser/QuantumultXParser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.Extensions.Logging; 6 | using ProxyStation.Model; 7 | using ProxyStation.ProfileParser.Template; 8 | using ProxyStation.Util; 9 | 10 | namespace ProxyStation.ProfileParser 11 | { 12 | public class QuantumultXEncodeOptions : EncodeOptions 13 | { 14 | public string QuantumultXListUrl { get; set; } 15 | } 16 | 17 | public class QuantumultXParser : IProfileParser 18 | { 19 | readonly ILogger logger; 20 | 21 | readonly IDownloader downloader; 22 | 23 | readonly GeneralParser fallbackParser; 24 | 25 | public QuantumultXParser(ILogger logger, IDownloader downloader) 26 | { 27 | this.downloader = downloader; 28 | this.logger = logger; 29 | this.fallbackParser = new GeneralParser(logger); 30 | } 31 | 32 | public bool ValidateTemplate(string template) 33 | { 34 | if (string.IsNullOrEmpty(template)) 35 | { 36 | return true; 37 | } 38 | 39 | return template.Contains(QuantumultX.ServerListUrlPlaceholder); 40 | } 41 | 42 | public string Encode(EncodeOptions options, Server[] servers, out Server[] encodedServers) 43 | { 44 | if (!this.ValidateTemplate(options.Template)) 45 | { 46 | throw new InvalidTemplateException(); 47 | } 48 | 49 | if (!(options is QuantumultXEncodeOptions opts)) 50 | { 51 | throw new ArgumentException(); 52 | } 53 | 54 | this.EncodeProxyList(servers, out encodedServers); 55 | 56 | var template = string.IsNullOrEmpty(options.Template) ? QuantumultX.Template : options.Template; 57 | var profile = template.Replace(QuantumultX.ServerListUrlPlaceholder, opts.QuantumultXListUrl); 58 | 59 | return profile; 60 | } 61 | 62 | public string EncodeShadowsocksServer(ShadowsocksServer server) 63 | { 64 | var properties = new List 65 | { 66 | $"shadowsocks={server.Host}:{server.Port}", 67 | $"method={server.Method}", 68 | $"password={server.Password}", 69 | }; 70 | 71 | switch (server.PluginOptions) 72 | { 73 | case V2RayPluginOptions options: 74 | if (options.Mode == V2RayPluginMode.QUIC) 75 | { 76 | // v2ray-plugin with QUIC is not supported 77 | return null; 78 | } 79 | else if (options.Mode == V2RayPluginMode.WebSocket) 80 | { 81 | properties.Add("obfs=" + (options.EnableTLS ? "wss" : "ws")); 82 | properties.Add($"obfs-host=" + (string.IsNullOrWhiteSpace(options.Host) ? Constant.ObfsucationHost : options.Host)); 83 | 84 | if (!string.IsNullOrWhiteSpace(options.Path) || options.Path == "/") 85 | { 86 | properties.Add($"obfs-uri={options.Path}"); 87 | } 88 | } 89 | break; 90 | case SimpleObfsPluginOptions options: 91 | properties.Add($"obfs={options.Mode.ToString().ToLower()}"); 92 | properties.Add($"obfs-host=" + (string.IsNullOrWhiteSpace(options.Host) ? Constant.ObfsucationHost : options.Host)); 93 | 94 | if (options.Mode == SimpleObfsPluginMode.HTTP) 95 | { 96 | properties.Add($"obfs-uri={options.Uri}"); 97 | } 98 | break; 99 | } 100 | 101 | properties.Add($"fast-open={server.FastOpen.ToString().ToLower()}"); 102 | properties.Add($"udp-relay={server.UDPRelay.ToString().ToLower()}"); 103 | properties.Add($"tag={server.Name}"); 104 | return string.Join(", ", properties); 105 | } 106 | 107 | public string EncodeShadowsocksRServer(ShadowsocksRServer server) 108 | { 109 | var properties = new List(); 110 | properties.Add($"shadowsocks={server.Host}:{server.Port}"); 111 | properties.Add($"method={server.Method}"); 112 | properties.Add($"password={server.Password}"); 113 | properties.Add($"ssr-protocol={server.Protocol}"); 114 | properties.Add($"ssr-protocol-param={server.ProtocolParameter}"); 115 | properties.Add($"obfs={server.Obfuscation}"); 116 | properties.Add($"obfs-host={server.ObfuscationParameter}"); 117 | properties.Add($"tag={server.Name}"); 118 | return string.Join(", ", properties); 119 | } 120 | 121 | public string EncodeProxyList(Server[] servers, out Server[] encodedServers) 122 | { 123 | var encodedServersList = new List(); 124 | var serverLines = servers 125 | .Select(server => 126 | { 127 | string serverLine = server switch 128 | { 129 | ShadowsocksServer ssServer => this.EncodeShadowsocksServer(ssServer), 130 | ShadowsocksRServer ssrServer => this.EncodeShadowsocksRServer(ssrServer), 131 | _ => null, 132 | }; 133 | if (!string.IsNullOrWhiteSpace(serverLine)) 134 | { 135 | encodedServersList.Add(server); 136 | } 137 | else 138 | { 139 | this.logger.LogInformation($"Server {server} is ignored."); 140 | } 141 | return serverLine; 142 | }) 143 | .Where(line => !string.IsNullOrWhiteSpace(line)) 144 | .ToArray(); 145 | 146 | encodedServers = encodedServersList.ToArray(); 147 | return string.Join('\n', serverLines); 148 | } 149 | 150 | public Server[] Parse(string profile) 151 | { 152 | var properties = Misc.ParsePropertieFile(profile); 153 | var servers = new List(); 154 | 155 | var downloadRemoteServerListTasks = properties.GetValueOrDefault("server_remote") 156 | ?.Select(s => 157 | { 158 | var parts = s.Split(","); 159 | if (parts.First(s => s.Trim().ToLower() == "enabled=false") != null) 160 | { 161 | return null; 162 | } 163 | return parts[0].Trim(); 164 | }) 165 | .Where(s => !string.IsNullOrWhiteSpace(s) && (s.StartsWith("https://") || s.StartsWith("http://") || s.StartsWith("ftp://"))) 166 | .Select(async url => 167 | { 168 | this.logger.LogInformation("Downloading remote servers from " + url); 169 | var plainProxies = await this.downloader.Download(this.logger, url); 170 | 171 | var remoteServers = this.ParseProxyList(plainProxies); 172 | this.logger.LogInformation($"Get {remoteServers.Length} remote servers from {url}"); 173 | servers.AddRange(remoteServers); 174 | }) 175 | .ToArray(); 176 | Task.WaitAll(downloadRemoteServerListTasks); 177 | 178 | var localServers = this.ParseProxyList(properties.GetValueOrDefault("server_local")); 179 | servers.AddRange(localServers); 180 | 181 | return servers.ToArray(); 182 | } 183 | 184 | public Server[] ParseProxyList(string plainProxies) 185 | { 186 | if (string.IsNullOrWhiteSpace(plainProxies)) return new Server[] { }; 187 | 188 | var servers = this.ParseProxyList(plainProxies.Trim().Split("\n")); 189 | if (servers.Length != 0) 190 | { 191 | return servers; 192 | } 193 | 194 | // Try GeneralParser 195 | return fallbackParser.Parse(plainProxies); 196 | } 197 | 198 | public Server[] ParseProxyList(IEnumerable plainProxies) 199 | { 200 | return plainProxies 201 | ?.Select(line => 202 | { 203 | line = line.Trim(); 204 | if (string.IsNullOrEmpty(line)) return null; 205 | else if (line.StartsWith("ss://")) return this.fallbackParser.ParseShadowsocksURI(line); 206 | else if (line.StartsWith("ssr://")) return this.fallbackParser.ParseShadowsocksURI(line); 207 | else if (line.StartsWith("shadowsocks=")) return this.ParseShadowsocksLine(line); 208 | else if (line.StartsWith("vmess=")) return null; 209 | else if (line.StartsWith("http=")) return null; 210 | else 211 | { 212 | this.logger.LogWarning($"[{{ComponentName}}] Unsupported proxy setting: {line}", this.GetType()); 213 | return null; 214 | } 215 | }) 216 | .Where(s => s != null) 217 | .ToArray(); 218 | } 219 | 220 | /// 221 | /// Parse QuantumultX shadowsocks proxy setting 222 | /// Sample: 223 | /// shadowsocks=example.com:80, method=chacha20, password=pwd, obfs=http, obfs-host=bing.com, obfs-uri=/resource/file, fast-open=false, udp-relay=false, server_check_url=http://www.apple.com/generate_204, tag=ss-01 224 | /// shadowsocks=example.com:443, method=chacha20, password=pwd, ssr-protocol=auth_chain_b, ssr-protocol-param=def, obfs=tls1.2_ticket_fastauth, obfs-host=bing.com, tag=ssr 225 | /// 226 | /// A string starts with `shadowsocks=` 227 | /// Could be an instance of or 228 | public Server ParseShadowsocksLine(string line) 229 | { 230 | var properties = new Dictionary(); 231 | foreach (var kvPair in line.Split(",")) 232 | { 233 | var parts = kvPair.Split("=", 2); 234 | if (parts.Length != 2) 235 | { 236 | this.logger.LogWarning($"[{{ComponentName}}] Invaild shadowsocks setting: {line}", this.GetType()); 237 | return null; 238 | } 239 | 240 | properties.TryAdd(parts[0].Trim().ToLower(), parts[1].Trim()); 241 | } 242 | 243 | var host = default(string); 244 | var port = default(int); 245 | var method = properties.GetValueOrDefault("method"); 246 | var password = properties.GetValueOrDefault("password"); 247 | 248 | if (!Misc.SplitHostAndPort(properties.GetValueOrDefault("shadowsocks"), out host, out port)) 249 | { 250 | this.logger.LogWarning($"[{{ComponentName}}] Host and port are invalid and this proxy setting is ignored. Invaild shadowsocks setting : {line}", this.GetType()); 251 | return null; 252 | } 253 | 254 | if (string.IsNullOrEmpty(method)) 255 | { 256 | this.logger.LogWarning($"[{{ComponentName}}] Encryption method is missing and this proxy setting is ignored. Invaild shadowsocks setting : {line}", this.GetType()); 257 | return null; 258 | } 259 | 260 | if (string.IsNullOrEmpty(password)) 261 | { 262 | this.logger.LogWarning($"[{{ComponentName}}] Password is missing and this proxy setting is ignored. Invaild shadowsocks setting : {line}", this.GetType()); 263 | return null; 264 | } 265 | 266 | Server server; 267 | 268 | if (this.IsShadowsocksRServer(properties)) 269 | { 270 | var ssrServer = new ShadowsocksRServer 271 | { 272 | Method = method, 273 | }; 274 | 275 | ssrServer.Obfuscation = properties.GetValueOrDefault("obfs"); 276 | ssrServer.ObfuscationParameter = properties.GetValueOrDefault("obfs-host"); 277 | ssrServer.Protocol = properties.GetValueOrDefault("ssr-protocol"); 278 | ssrServer.ProtocolParameter = properties.GetValueOrDefault("ssr-protocol-param"); 279 | // QuantumultX doesn't support following parameter yet 280 | // * UDP Port 281 | // * UDP over TCP 282 | 283 | server = ssrServer; 284 | } 285 | else 286 | { 287 | var ssServer = new ShadowsocksServer 288 | { 289 | Method = method, 290 | }; 291 | 292 | if (bool.TryParse(properties.GetValueOrDefault("udp-relay", "false"), out bool udpRelay)) 293 | { 294 | ssServer.UDPRelay = udpRelay; 295 | } 296 | 297 | if (bool.TryParse(properties.GetValueOrDefault("fast-open", "false"), out bool fastOpen)) 298 | { 299 | ssServer.FastOpen = fastOpen; 300 | } 301 | 302 | var obfs = properties.GetValueOrDefault("obfs", "").ToLower(); 303 | switch (obfs) 304 | { 305 | case "tls": 306 | case "http": 307 | { 308 | if (!SimpleObfsPluginOptions.TryParseMode(obfs, out SimpleObfsPluginMode mode)) 309 | { 310 | this.logger.LogWarning($"[{{ComponentName}}] Simple-obfs mode `{obfs}` is not supported and this proxy setting is ignored. Invaild shadowsocks setting : {line}", this.GetType()); 311 | return null; 312 | } 313 | 314 | var options = new SimpleObfsPluginOptions() 315 | { 316 | Host = properties.GetValueOrDefault("obfs-host"), 317 | Mode = mode, 318 | }; 319 | 320 | if (obfs != "tls") 321 | { 322 | options.Uri = properties.GetValueOrDefault("obfs-uri"); 323 | } 324 | 325 | ssServer.PluginOptions = options; 326 | } 327 | break; 328 | case "wss": 329 | case "ws": 330 | { 331 | if (!V2RayPluginOptions.TryParseMode(obfs, out V2RayPluginMode mode)) 332 | { 333 | this.logger.LogWarning($"[{{ComponentName}}] Simple-obfs mode `{obfs}` is not supported and this proxy setting is ignored. Invaild shadowsocks setting : {line}", this.GetType()); 334 | return null; 335 | } 336 | 337 | var options = new V2RayPluginOptions() 338 | { 339 | Mode = mode, 340 | Host = properties.GetValueOrDefault("obfs-host"), 341 | Path = properties.GetValueOrDefault("obfs-uri"), 342 | EnableTLS = obfs == "wss", 343 | }; 344 | 345 | ssServer.PluginOptions = options; 346 | } 347 | break; 348 | default: 349 | this.logger.LogWarning($"[{{ComponentName}}] Obfuscation `{obfs}` is not supported and this proxy setting is ignored. Invaild shadowsocks setting : {line}", this.GetType()); 350 | return null; 351 | } 352 | 353 | server = ssServer; 354 | } 355 | 356 | server.Host = host; 357 | server.Port = port; 358 | server.Name = properties.GetValueOrDefault("tag", $"{server.Host}:{server.Port}"); 359 | 360 | return server; 361 | } 362 | 363 | private bool IsShadowsocksRServer(Dictionary properties) 364 | { 365 | if (properties.ContainsKey("ssr-protocol")) 366 | { 367 | return true; 368 | } 369 | 370 | return false; 371 | } 372 | 373 | // TODO: VMess Parser 374 | // ;vmess=example.com:80, method=none, password=23ad6b10-8d1a-40f7-8ad0-e3e35cd32291, fast-open=false, udp-relay=false, tag=vmess-01 375 | // ;vmess=example.com:80, method=aes-128-gcm, password=23ad6b10-8d1a-40f7-8ad0-e3e35cd32291, fast-open=false, udp-relay=false, tag=vmess-02 376 | // ;vmess=example.com:443, method=none, password=23ad6b10-8d1a-40f7-8ad0-e3e35cd32291, obfs=over-tls, fast-open=false, udp-relay=false, tag=vmess-tls 377 | // ;vmess=example.com:80, method=chacha20-poly1305, password=23ad6b10-8d1a-40f7-8ad0-e3e35cd32291, obfs=ws, obfs-uri=/ws, fast-open=false, udp-relay=false, tag=vmess-ws 378 | // ;vmess=example.com:443, method=chacha20-poly1305, password=23ad6b10-8d1a-40f7-8ad0-e3e35cd32291, obfs=wss, obfs-uri=/ws, fast-open=false, udp-relay=false, tag=vmess-ws-tls 379 | // public Server ParseVMessURI(string uri) { 380 | 381 | // } 382 | 383 | public string ExtName() => ".conf"; 384 | } 385 | } --------------------------------------------------------------------------------