- Shadowsocks: some fundamental stuff - Shadowsocks.Net: networking & cryptography library - Shadowsocks.PAC: garbage - Shadowsocks.Protobuf: geosite + geoip parsing - Shadowsocks.WPF: WPF GUIpull/2897/head
@@ -1,9 +0,0 @@ | |||||
using SimpleInjector; | |||||
namespace Shadowsocks.Common.Model | |||||
{ | |||||
public static class IoCManager | |||||
{ | |||||
public static Container Container { get; } = new Container(); | |||||
} | |||||
} |
@@ -1,17 +0,0 @@ | |||||
<Project Sdk="Microsoft.NET.Sdk"> | |||||
<PropertyGroup> | |||||
<TargetFramework>netstandard2.1</TargetFramework> | |||||
<Authors>clowwindy & community 2020</Authors> | |||||
<Company>clowwindy & community 2020</Company> | |||||
<Product>Shadowsocks Common</Product> | |||||
</PropertyGroup> | |||||
<ItemGroup> | |||||
<PackageReference Include="Google.Protobuf" Version="3.13.0" /> | |||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" /> | |||||
<PackageReference Include="NLog" Version="4.7.5" /> | |||||
<PackageReference Include="SimpleInjector" Version="5.0.4" /> | |||||
</ItemGroup> | |||||
</Project> |
@@ -1,18 +0,0 @@ | |||||
<Project Sdk="Microsoft.NET.Sdk"> | |||||
<PropertyGroup> | |||||
<TargetFramework>netstandard2.1</TargetFramework> | |||||
<Authors>clowwindy & community 2020</Authors> | |||||
<Product>Shadowsocks Crypto</Product> | |||||
</PropertyGroup> | |||||
<ItemGroup> | |||||
<PackageReference Include="BouncyCastle.NetCore" Version="1.8.6" /> | |||||
<PackageReference Include="NaCl.Core" Version="2.0.0" /> | |||||
</ItemGroup> | |||||
<ItemGroup> | |||||
<ProjectReference Include="..\Shadowsocks.Common\Shadowsocks.Common.csproj" /> | |||||
</ItemGroup> | |||||
</Project> |
@@ -1,4 +1,4 @@ | |||||
using System; | |||||
using System; | |||||
using System.Collections.Generic; | using System.Collections.Generic; | ||||
using System.IO; | using System.IO; | ||||
using System.Net.Sockets; | using System.Net.Sockets; | ||||
@@ -7,7 +7,7 @@ using System.Text; | |||||
using System.Threading; | using System.Threading; | ||||
using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
namespace Shadowsocks.Controller | |||||
namespace Shadowsocks.Net | |||||
{ | { | ||||
// cache first packet for duty-chain pattern listener | // cache first packet for duty-chain pattern listener | ||||
public class CachedNetworkStream : Stream | public class CachedNetworkStream : Stream |
@@ -2,7 +2,7 @@ using System; | |||||
using System.Collections.Generic; | using System.Collections.Generic; | ||||
using System.Security.Cryptography; | using System.Security.Cryptography; | ||||
namespace Shadowsocks.Crypto.AEAD | |||||
namespace Shadowsocks.Net.Crypto.AEAD | |||||
{ | { | ||||
public class AEADAesGcmNativeCrypto : AEADCrypto | public class AEADAesGcmNativeCrypto : AEADCrypto | ||||
{ | { |
@@ -1,11 +1,10 @@ | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using Org.BouncyCastle.Crypto.Engines; | using Org.BouncyCastle.Crypto.Engines; | ||||
using Org.BouncyCastle.Crypto.Modes; | using Org.BouncyCastle.Crypto.Modes; | ||||
using Org.BouncyCastle.Crypto.Parameters; | using Org.BouncyCastle.Crypto.Parameters; | ||||
using System; | |||||
using System.Collections.Generic; | |||||
namespace Shadowsocks.Crypto.AEAD | |||||
namespace Shadowsocks.Net.Crypto.AEAD | |||||
{ | { | ||||
public class AEADBouncyCastleCrypto : AEADCrypto | public class AEADBouncyCastleCrypto : AEADCrypto | ||||
{ | { |
@@ -1,15 +1,12 @@ | |||||
using Shadowsocks.Net.Crypto.Exception; | |||||
using Shadowsocks.Net.Crypto.Stream; | |||||
using System; | using System; | ||||
using System.Collections.Generic; | using System.Collections.Generic; | ||||
using System.Net; | using System.Net; | ||||
using System.Runtime.CompilerServices; | using System.Runtime.CompilerServices; | ||||
using System.Text; | using System.Text; | ||||
using NLog; | |||||
using Shadowsocks.Crypto.Exception; | |||||
using Shadowsocks.Crypto.Stream; | |||||
namespace Shadowsocks.Crypto.AEAD | |||||
namespace Shadowsocks.Net.Crypto.AEAD | |||||
{ | { | ||||
public abstract class AEADCrypto : CryptoBase | public abstract class AEADCrypto : CryptoBase | ||||
{ | { |
@@ -1,10 +1,9 @@ | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using NaCl.Core; | using NaCl.Core; | ||||
using NaCl.Core.Base; | using NaCl.Core.Base; | ||||
using System; | |||||
using System.Collections.Generic; | |||||
namespace Shadowsocks.Crypto.AEAD | |||||
namespace Shadowsocks.Net.Crypto.AEAD | |||||
{ | { | ||||
public class AEADNaClCrypto : AEADCrypto | public class AEADNaClCrypto : AEADCrypto | ||||
{ | { |
@@ -1,4 +1,4 @@ | |||||
namespace Shadowsocks.Crypto | |||||
namespace Shadowsocks.Net.Crypto | |||||
{ | { | ||||
public enum CipherFamily | public enum CipherFamily | ||||
{ | { |
@@ -1,6 +1,6 @@ | |||||
using System; | using System; | ||||
namespace Shadowsocks.Crypto | |||||
namespace Shadowsocks.Net.Crypto | |||||
{ | { | ||||
public abstract class CryptoBase : ICrypto | public abstract class CryptoBase : ICrypto | ||||
{ | { |
@@ -3,10 +3,10 @@ using System.Collections.Generic; | |||||
using System.Reflection; | using System.Reflection; | ||||
using System.Text; | using System.Text; | ||||
using Shadowsocks.Crypto.AEAD; | |||||
using Shadowsocks.Crypto.Stream; | |||||
using Shadowsocks.Net.Crypto.AEAD; | |||||
using Shadowsocks.Net.Crypto.Stream; | |||||
namespace Shadowsocks.Crypto | |||||
namespace Shadowsocks.Net.Crypto | |||||
{ | { | ||||
public static class CryptoFactory | public static class CryptoFactory | ||||
{ | { |
@@ -6,7 +6,7 @@ using System; | |||||
using System.Security.Cryptography; | using System.Security.Cryptography; | ||||
using System.Threading; | using System.Threading; | ||||
namespace Shadowsocks.Crypto | |||||
namespace Shadowsocks.Net.Crypto | |||||
{ | { | ||||
public static class CryptoUtils | public static class CryptoUtils | ||||
{ | { |
@@ -1,4 +1,4 @@ | |||||
namespace Shadowsocks.Crypto.Exception | |||||
namespace Shadowsocks.Net.Crypto.Exception | |||||
{ | { | ||||
public class CryptoErrorException : System.Exception | public class CryptoErrorException : System.Exception | ||||
{ | { |
@@ -1,6 +1,6 @@ | |||||
using System; | using System; | ||||
namespace Shadowsocks.Crypto | |||||
namespace Shadowsocks.Net.Crypto | |||||
{ | { | ||||
public interface ICrypto | public interface ICrypto | ||||
{ | { |
@@ -1,7 +1,7 @@ | |||||
using System; | using System; | ||||
using System.Security.Cryptography; | using System.Security.Cryptography; | ||||
namespace Shadowsocks.Crypto | |||||
namespace Shadowsocks.Net.Crypto | |||||
{ | { | ||||
public static class RNG | public static class RNG | ||||
{ | { |
@@ -5,7 +5,7 @@ using System; | |||||
using System.Collections.Generic; | using System.Collections.Generic; | ||||
using System.Runtime.CompilerServices; | using System.Runtime.CompilerServices; | ||||
namespace Shadowsocks.Crypto.Stream | |||||
namespace Shadowsocks.Net.Crypto.Stream | |||||
{ | { | ||||
public class StreamAesCfbBouncyCastleCrypto : StreamCrypto | public class StreamAesCfbBouncyCastleCrypto : StreamCrypto |
@@ -3,7 +3,7 @@ using System.Collections.Generic; | |||||
using System.Runtime.CompilerServices; | using System.Runtime.CompilerServices; | ||||
using NaCl.Core; | using NaCl.Core; | ||||
namespace Shadowsocks.Crypto.Stream | |||||
namespace Shadowsocks.Net.Crypto.Stream | |||||
{ | { | ||||
public class StreamChachaNaClCrypto : StreamCrypto | public class StreamChachaNaClCrypto : StreamCrypto | ||||
{ | { |
@@ -1,11 +1,9 @@ | |||||
using NLog; | |||||
using System; | using System; | ||||
using System.Collections.Generic; | using System.Collections.Generic; | ||||
using System.Runtime.CompilerServices; | using System.Runtime.CompilerServices; | ||||
using System.Text; | using System.Text; | ||||
namespace Shadowsocks.Crypto.Stream | |||||
namespace Shadowsocks.Net.Crypto.Stream | |||||
{ | { | ||||
public abstract class StreamCrypto : CryptoBase | public abstract class StreamCrypto : CryptoBase | ||||
{ | { |
@@ -1,7 +1,7 @@ | |||||
using System; | using System; | ||||
using System.Collections.Generic; | using System.Collections.Generic; | ||||
namespace Shadowsocks.Crypto.Stream | |||||
namespace Shadowsocks.Net.Crypto.Stream | |||||
{ | { | ||||
public class StreamPlainNativeCrypto : StreamCrypto | public class StreamPlainNativeCrypto : StreamCrypto | ||||
{ | { |
@@ -2,7 +2,7 @@ using System; | |||||
using System.Collections.Generic; | using System.Collections.Generic; | ||||
using System.Runtime.CompilerServices; | using System.Runtime.CompilerServices; | ||||
namespace Shadowsocks.Crypto.Stream | |||||
namespace Shadowsocks.Net.Crypto.Stream | |||||
{ | { | ||||
public class StreamRc4NativeCrypto : StreamCrypto | public class StreamRc4NativeCrypto : StreamCrypto | ||||
{ | { |
@@ -1,6 +1,6 @@ | |||||
using Shadowsocks.Crypto.AEAD; | |||||
using Shadowsocks.Net.Crypto.AEAD; | |||||
namespace Shadowsocks.Crypto | |||||
namespace Shadowsocks.Net.Crypto | |||||
{ | { | ||||
public static class TCPParameter | public static class TCPParameter | ||||
{ | { |
@@ -1,11 +1,10 @@ | |||||
using System; | |||||
using System; | |||||
using System.Net; | using System.Net; | ||||
using System.Net.Sockets; | using System.Net.Sockets; | ||||
using System.Threading; | using System.Threading; | ||||
using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
using Shadowsocks.Util.Sockets; | |||||
namespace Shadowsocks.Proxy | |||||
namespace Shadowsocks.Net.Proxy | |||||
{ | { | ||||
public class DirectConnect : IProxy | public class DirectConnect : IProxy | ||||
{ | { |
@@ -1,117 +1,115 @@ | |||||
using System; | |||||
using System.Net; | |||||
using System.Net.Sockets; | |||||
using System.Text; | |||||
using System.Text.RegularExpressions; | |||||
using System.Threading; | |||||
using System.Threading.Tasks; | |||||
using NLog; | |||||
using Shadowsocks.Controller; | |||||
using Shadowsocks.Util.Sockets; | |||||
namespace Shadowsocks.Proxy | |||||
{ | |||||
public class HttpProxy : IProxy | |||||
{ | |||||
private static readonly Logger logger = LogManager.GetCurrentClassLogger(); | |||||
public EndPoint LocalEndPoint => _remote.LocalEndPoint; | |||||
public EndPoint ProxyEndPoint { get; private set; } | |||||
public EndPoint DestEndPoint { get; private set; } | |||||
private readonly Socket _remote = new Socket(SocketType.Stream, ProtocolType.Tcp); | |||||
private const string HTTP_CRLF = "\r\n"; | |||||
private const string HTTP_CONNECT_TEMPLATE = | |||||
"CONNECT {0} HTTP/1.1" + HTTP_CRLF + | |||||
"Host: {0}" + HTTP_CRLF + | |||||
"Proxy-Connection: keep-alive" + HTTP_CRLF + | |||||
"User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36" + HTTP_CRLF + | |||||
"{1}" + // Proxy-Authorization if any | |||||
"" + HTTP_CRLF; // End with an empty line | |||||
private const string PROXY_AUTH_TEMPLATE = "Proxy-Authorization: Basic {0}" + HTTP_CRLF; | |||||
public void Shutdown(SocketShutdown how) | |||||
{ | |||||
_remote.Shutdown(how); | |||||
} | |||||
public void Close() | |||||
{ | |||||
_remote.Dispose(); | |||||
} | |||||
private static readonly Regex HttpRespondHeaderRegex = new Regex(@"^(HTTP/1\.\d) (\d{3}) (.+)$", RegexOptions.Compiled); | |||||
private int _respondLineCount = 0; | |||||
private bool _established = false; | |||||
private bool OnLineRead(string line, object state) | |||||
{ | |||||
logger.Trace(line); | |||||
if (_respondLineCount == 0) | |||||
{ | |||||
var m = HttpRespondHeaderRegex.Match(line); | |||||
if (m.Success) | |||||
{ | |||||
var resultCode = m.Groups[2].Value; | |||||
if ("200" != resultCode) | |||||
{ | |||||
return true; | |||||
} | |||||
_established = true; | |||||
} | |||||
} | |||||
else | |||||
{ | |||||
if (string.IsNullOrEmpty(line)) | |||||
{ | |||||
return true; | |||||
} | |||||
} | |||||
_respondLineCount++; | |||||
return false; | |||||
} | |||||
private NetworkCredential auth; | |||||
public async Task ConnectProxyAsync(EndPoint remoteEP, NetworkCredential auth = null, CancellationToken token = default) | |||||
{ | |||||
ProxyEndPoint = remoteEP; | |||||
this.auth = auth; | |||||
await _remote.ConnectAsync(remoteEP); | |||||
_remote.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.NoDelay, true); | |||||
} | |||||
public async Task ConnectRemoteAsync(EndPoint destEndPoint, CancellationToken token = default) | |||||
{ | |||||
DestEndPoint = destEndPoint; | |||||
String authInfo = ""; | |||||
if (auth != null) | |||||
{ | |||||
string authKey = Convert.ToBase64String(Encoding.UTF8.GetBytes(auth.UserName + ":" + auth.Password)); | |||||
authInfo = string.Format(PROXY_AUTH_TEMPLATE, authKey); | |||||
} | |||||
string request = string.Format(HTTP_CONNECT_TEMPLATE, destEndPoint, authInfo); | |||||
var b = Encoding.UTF8.GetBytes(request); | |||||
await _remote.SendAsync(Encoding.UTF8.GetBytes(request), SocketFlags.None, token); | |||||
// start line read | |||||
LineReader reader = new LineReader(_remote, OnLineRead, (e, _) => throw e, (_1, _2, _3, _4) => { }, Encoding.UTF8, HTTP_CRLF, 1024, null); | |||||
await reader.Finished; | |||||
} | |||||
public async Task<int> SendAsync(ReadOnlyMemory<byte> buffer, CancellationToken token = default) | |||||
{ | |||||
return await _remote.SendAsync(buffer, SocketFlags.None, token); | |||||
} | |||||
public async Task<int> ReceiveAsync(Memory<byte> buffer, CancellationToken token = default) | |||||
{ | |||||
return await _remote.ReceiveAsync(buffer, SocketFlags.None, token); | |||||
} | |||||
} | |||||
} | |||||
using System; | |||||
using System.Net; | |||||
using System.Net.Sockets; | |||||
using System.Text; | |||||
using System.Text.RegularExpressions; | |||||
using System.Threading; | |||||
using System.Threading.Tasks; | |||||
using NLog; | |||||
namespace Shadowsocks.Net.Proxy | |||||
{ | |||||
public class HttpProxy : IProxy | |||||
{ | |||||
private static readonly Logger logger = LogManager.GetCurrentClassLogger(); | |||||
public EndPoint LocalEndPoint => _remote.LocalEndPoint; | |||||
public EndPoint ProxyEndPoint { get; private set; } | |||||
public EndPoint DestEndPoint { get; private set; } | |||||
private readonly Socket _remote = new Socket(SocketType.Stream, ProtocolType.Tcp); | |||||
private const string HTTP_CRLF = "\r\n"; | |||||
private const string HTTP_CONNECT_TEMPLATE = | |||||
"CONNECT {0} HTTP/1.1" + HTTP_CRLF + | |||||
"Host: {0}" + HTTP_CRLF + | |||||
"Proxy-Connection: keep-alive" + HTTP_CRLF + | |||||
"User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36" + HTTP_CRLF + | |||||
"{1}" + // Proxy-Authorization if any | |||||
"" + HTTP_CRLF; // End with an empty line | |||||
private const string PROXY_AUTH_TEMPLATE = "Proxy-Authorization: Basic {0}" + HTTP_CRLF; | |||||
public void Shutdown(SocketShutdown how) | |||||
{ | |||||
_remote.Shutdown(how); | |||||
} | |||||
public void Close() | |||||
{ | |||||
_remote.Dispose(); | |||||
} | |||||
private static readonly Regex HttpRespondHeaderRegex = new Regex(@"^(HTTP/1\.\d) (\d{3}) (.+)$", RegexOptions.Compiled); | |||||
private int _respondLineCount = 0; | |||||
private bool _established = false; | |||||
private bool OnLineRead(string line, object state) | |||||
{ | |||||
logger.Trace(line); | |||||
if (_respondLineCount == 0) | |||||
{ | |||||
var m = HttpRespondHeaderRegex.Match(line); | |||||
if (m.Success) | |||||
{ | |||||
var resultCode = m.Groups[2].Value; | |||||
if ("200" != resultCode) | |||||
{ | |||||
return true; | |||||
} | |||||
_established = true; | |||||
} | |||||
} | |||||
else | |||||
{ | |||||
if (string.IsNullOrEmpty(line)) | |||||
{ | |||||
return true; | |||||
} | |||||
} | |||||
_respondLineCount++; | |||||
return false; | |||||
} | |||||
private NetworkCredential auth; | |||||
public async Task ConnectProxyAsync(EndPoint remoteEP, NetworkCredential auth = null, CancellationToken token = default) | |||||
{ | |||||
ProxyEndPoint = remoteEP; | |||||
this.auth = auth; | |||||
await _remote.ConnectAsync(remoteEP); | |||||
_remote.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.NoDelay, true); | |||||
} | |||||
public async Task ConnectRemoteAsync(EndPoint destEndPoint, CancellationToken token = default) | |||||
{ | |||||
DestEndPoint = destEndPoint; | |||||
String authInfo = ""; | |||||
if (auth != null) | |||||
{ | |||||
string authKey = Convert.ToBase64String(Encoding.UTF8.GetBytes(auth.UserName + ":" + auth.Password)); | |||||
authInfo = string.Format(PROXY_AUTH_TEMPLATE, authKey); | |||||
} | |||||
string request = string.Format(HTTP_CONNECT_TEMPLATE, destEndPoint, authInfo); | |||||
var b = Encoding.UTF8.GetBytes(request); | |||||
await _remote.SendAsync(Encoding.UTF8.GetBytes(request), SocketFlags.None, token); | |||||
// start line read | |||||
LineReader reader = new LineReader(_remote, OnLineRead, (e, _) => throw e, (_1, _2, _3, _4) => { }, Encoding.UTF8, HTTP_CRLF, 1024, null); | |||||
await reader.Finished; | |||||
} | |||||
public async Task<int> SendAsync(ReadOnlyMemory<byte> buffer, CancellationToken token = default) | |||||
{ | |||||
return await _remote.SendAsync(buffer, SocketFlags.None, token); | |||||
} | |||||
public async Task<int> ReceiveAsync(Memory<byte> buffer, CancellationToken token = default) | |||||
{ | |||||
return await _remote.ReceiveAsync(buffer, SocketFlags.None, token); | |||||
} | |||||
} | |||||
} |
@@ -1,12 +1,11 @@ | |||||
using System; | |||||
using System; | |||||
using System.Net; | using System.Net; | ||||
using System.Net.Sockets; | using System.Net.Sockets; | ||||
using System.Threading; | using System.Threading; | ||||
using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
namespace Shadowsocks.Proxy | |||||
namespace Shadowsocks.Net.Proxy | |||||
{ | { | ||||
public interface IProxy | public interface IProxy | ||||
{ | { | ||||
EndPoint LocalEndPoint { get; } | EndPoint LocalEndPoint { get; } |
@@ -1,9 +1,9 @@ | |||||
using System; | |||||
using System; | |||||
using System.Net.Sockets; | using System.Net.Sockets; | ||||
using System.Text; | using System.Text; | ||||
using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
namespace Shadowsocks.Util.Sockets | |||||
namespace Shadowsocks.Net.Proxy | |||||
{ | { | ||||
public class LineReader | public class LineReader | ||||
{ | { |
@@ -1,13 +1,11 @@ | |||||
using System; | |||||
using System; | |||||
using System.Net; | using System.Net; | ||||
using System.Net.Sockets; | using System.Net.Sockets; | ||||
using System.Text; | using System.Text; | ||||
using System.Threading; | using System.Threading; | ||||
using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
using Shadowsocks.Controller; | |||||
using Shadowsocks.Util.Sockets; | |||||
namespace Shadowsocks.Proxy | |||||
namespace Shadowsocks.Net.Proxy | |||||
{ | { | ||||
public class Socks5Proxy : IProxy | public class Socks5Proxy : IProxy | ||||
{ | { | ||||
@@ -37,11 +35,11 @@ namespace Shadowsocks.Proxy | |||||
await _remote.SendAsync(new byte[] { 5, 1, 0 }, SocketFlags.None); | await _remote.SendAsync(new byte[] { 5, 1, 0 }, SocketFlags.None); | ||||
if (await _remote.ReceiveAsync(_receiveBuffer.AsMemory(0, 2), SocketFlags.None) != 2) | if (await _remote.ReceiveAsync(_receiveBuffer.AsMemory(0, 2), SocketFlags.None) != 2) | ||||
{ | { | ||||
throw new Exception(I18N.GetString("Proxy handshake failed")); | |||||
throw new Exception("Proxy handshake failed"); | |||||
} | } | ||||
if (_receiveBuffer[0] != 5 || _receiveBuffer[1] != 0) | if (_receiveBuffer[0] != 5 || _receiveBuffer[1] != 0) | ||||
{ | { | ||||
throw new Exception(I18N.GetString("Proxy handshake failed")); | |||||
throw new Exception("Proxy handshake failed"); | |||||
} | } | ||||
} | } | ||||
@@ -80,14 +78,13 @@ namespace Shadowsocks.Proxy | |||||
atyp = 4; // IP V6 address | atyp = 4; // IP V6 address | ||||
break; | break; | ||||
default: | default: | ||||
throw new Exception(I18N.GetString("Proxy request failed")); | |||||
throw new Exception("Proxy request failed"); | |||||
} | } | ||||
port = ((IPEndPoint)DestEndPoint).Port; | port = ((IPEndPoint)DestEndPoint).Port; | ||||
var addr = ((IPEndPoint)DestEndPoint).Address.GetAddressBytes(); | var addr = ((IPEndPoint)DestEndPoint).Address.GetAddressBytes(); | ||||
Array.Copy(addr, 0, request, 4, request.Length - 4 - 2); | Array.Copy(addr, 0, request, 4, request.Length - 4 - 2); | ||||
} | } | ||||
// 构造request包剩余部分 | |||||
request[0] = 5; | request[0] = 5; | ||||
request[1] = 1; | request[1] = 1; | ||||
request[2] = 0; | request[2] = 0; | ||||
@@ -99,11 +96,11 @@ namespace Shadowsocks.Proxy | |||||
if (await _remote.ReceiveAsync(_receiveBuffer.AsMemory(0, 4), SocketFlags.None, token) != 4) | if (await _remote.ReceiveAsync(_receiveBuffer.AsMemory(0, 4), SocketFlags.None, token) != 4) | ||||
{ | { | ||||
throw new Exception(I18N.GetString("Proxy request failed")); | |||||
throw new Exception("Proxy request failed"); | |||||
}; | }; | ||||
if (_receiveBuffer[0] != 5 || _receiveBuffer[1] != 0) | if (_receiveBuffer[0] != 5 || _receiveBuffer[1] != 0) | ||||
{ | { | ||||
throw new Exception(I18N.GetString("Proxy request failed")); | |||||
throw new Exception("Proxy request failed"); | |||||
} | } | ||||
var addrLen = _receiveBuffer[3] switch | var addrLen = _receiveBuffer[3] switch | ||||
{ | { | ||||
@@ -113,11 +110,8 @@ namespace Shadowsocks.Proxy | |||||
}; | }; | ||||
if (await _remote.ReceiveAsync(_receiveBuffer.AsMemory(0, addrLen), SocketFlags.None, token) != addrLen) | if (await _remote.ReceiveAsync(_receiveBuffer.AsMemory(0, addrLen), SocketFlags.None, token) != addrLen) | ||||
{ | { | ||||
throw new Exception(I18N.GetString("Proxy request failed")); | |||||
throw new Exception("Proxy request failed"); | |||||
} | } | ||||
} | } | ||||
public async Task<int> SendAsync(ReadOnlyMemory<byte> buffer, CancellationToken token = default) | public async Task<int> SendAsync(ReadOnlyMemory<byte> buffer, CancellationToken token = default) |
@@ -0,0 +1,12 @@ | |||||
<Project Sdk="Microsoft.NET.Sdk"> | |||||
<PropertyGroup> | |||||
<TargetFramework>netcoreapp3.1</TargetFramework> | |||||
</PropertyGroup> | |||||
<ItemGroup> | |||||
<PackageReference Include="BouncyCastle.NetCore" Version="1.8.6" /> | |||||
<PackageReference Include="NaCl.Core" Version="2.0.0" /> | |||||
</ItemGroup> | |||||
</Project> |
@@ -1,9 +1,9 @@ | |||||
using System; | |||||
using System; | |||||
using System.Runtime.Serialization; | using System.Runtime.Serialization; | ||||
namespace Shadowsocks.Common.SystemProxy | |||||
namespace Shadowsocks.Net.SystemProxy | |||||
{ | { | ||||
enum ProxyExceptionType | |||||
public enum ProxyExceptionType | |||||
{ | { | ||||
Unspecific, | Unspecific, | ||||
FailToRun, | FailToRun, | ||||
@@ -12,7 +12,7 @@ namespace Shadowsocks.Common.SystemProxy | |||||
QueryReturnMalformed | QueryReturnMalformed | ||||
} | } | ||||
class ProxyException : Exception | |||||
public class ProxyException : Exception | |||||
{ | { | ||||
// provide more specific information about exception | // provide more specific information about exception | ||||
public ProxyExceptionType Type { get; } | public ProxyExceptionType Type { get; } |
@@ -1,5 +1,4 @@ | |||||
using NLog; | |||||
using Shadowsocks.Model; | |||||
using NLog; | |||||
using System; | using System; | ||||
using System.Collections.Generic; | using System.Collections.Generic; | ||||
using System.Linq; | using System.Linq; | ||||
@@ -7,7 +6,7 @@ using System.Net; | |||||
using System.Net.NetworkInformation; | using System.Net.NetworkInformation; | ||||
using System.Net.Sockets; | using System.Net.Sockets; | ||||
namespace Shadowsocks.Controller | |||||
namespace Shadowsocks.Net | |||||
{ | { | ||||
public interface IStreamService | public interface IStreamService | ||||
{ | { |
@@ -1,232 +1,230 @@ | |||||
using System; | |||||
using System.Buffers; | |||||
using System.Collections.Generic; | |||||
using System.Net; | |||||
using System.Net.Sockets; | |||||
using System.Runtime.CompilerServices; | |||||
using System.Runtime.InteropServices; | |||||
using System.Threading.Tasks; | |||||
using NLog; | |||||
using Shadowsocks.Controller.Strategy; | |||||
using Shadowsocks.Encryption; | |||||
using Shadowsocks.Model; | |||||
namespace Shadowsocks.Controller | |||||
{ | |||||
class UDPRelay : DatagramService | |||||
{ | |||||
private ShadowsocksController _controller; | |||||
// TODO: choose a smart number | |||||
private LRUCache<IPEndPoint, UDPHandler> _cache = new LRUCache<IPEndPoint, UDPHandler>(512); | |||||
public long outbound = 0; | |||||
public long inbound = 0; | |||||
public UDPRelay(ShadowsocksController controller) | |||||
{ | |||||
this._controller = controller; | |||||
} | |||||
public override async Task<bool> Handle(Memory<byte> packet, Socket socket, EndPoint client) | |||||
{ | |||||
if (socket.ProtocolType != ProtocolType.Udp) | |||||
{ | |||||
return false; | |||||
} | |||||
if (packet.Length < 4) | |||||
{ | |||||
return false; | |||||
} | |||||
IPEndPoint remoteEndPoint = (IPEndPoint)client; | |||||
UDPHandler handler = _cache.get(remoteEndPoint); | |||||
if (handler == null) | |||||
{ | |||||
handler = new UDPHandler(socket, _controller.GetAServer(IStrategyCallerType.UDP, remoteEndPoint, null/*TODO: fix this*/), remoteEndPoint); | |||||
handler.Receive(); | |||||
_cache.add(remoteEndPoint, handler); | |||||
} | |||||
await handler.SendAsync(packet); | |||||
return true; | |||||
} | |||||
public class UDPHandler | |||||
{ | |||||
private static Logger logger = LogManager.GetCurrentClassLogger(); | |||||
private static MemoryPool<byte> pool = MemoryPool<byte>.Shared; | |||||
private Socket _local; | |||||
private Socket _remote; | |||||
private Server _server; | |||||
private byte[] _buffer = new byte[65536]; | |||||
private IPEndPoint _localEndPoint; | |||||
private IPEndPoint _remoteEndPoint; | |||||
private IPAddress ListenAddress | |||||
{ | |||||
get | |||||
{ | |||||
return _remote.AddressFamily switch | |||||
{ | |||||
AddressFamily.InterNetwork => IPAddress.Any, | |||||
AddressFamily.InterNetworkV6 => IPAddress.IPv6Any, | |||||
_ => throw new NotSupportedException(), | |||||
}; | |||||
} | |||||
} | |||||
public UDPHandler(Socket local, Server server, IPEndPoint localEndPoint) | |||||
{ | |||||
_local = local; | |||||
_server = server; | |||||
_localEndPoint = localEndPoint; | |||||
// TODO async resolving | |||||
bool parsed = IPAddress.TryParse(server.server, out IPAddress ipAddress); | |||||
if (!parsed) | |||||
{ | |||||
IPHostEntry ipHostInfo = Dns.GetHostEntry(server.server); | |||||
ipAddress = ipHostInfo.AddressList[0]; | |||||
} | |||||
_remoteEndPoint = new IPEndPoint(ipAddress, server.server_port); | |||||
_remote = new Socket(_remoteEndPoint.AddressFamily, SocketType.Dgram, ProtocolType.Udp); | |||||
_remote.Bind(new IPEndPoint(ListenAddress, 0)); | |||||
} | |||||
public async Task SendAsync(ReadOnlyMemory<byte> data) | |||||
{ | |||||
IEncryptor encryptor = EncryptorFactory.GetEncryptor(_server.method, _server.password); | |||||
using IMemoryOwner<byte> mem = pool.Rent(data.Length + 1000); | |||||
// byte[] dataOut = new byte[slicedData.Length + 1000]; | |||||
int outlen = encryptor.EncryptUDP(data.Span[3..], mem.Memory.Span); | |||||
logger.Debug(_localEndPoint, _remoteEndPoint, outlen, "UDP Relay up"); | |||||
if (!MemoryMarshal.TryGetArray(mem.Memory[..outlen], out ArraySegment<byte> outData)) | |||||
{ | |||||
throw new InvalidOperationException("Can't extract underly array segment"); | |||||
}; | |||||
await _remote?.SendToAsync(outData, SocketFlags.None, _remoteEndPoint); | |||||
} | |||||
public async Task ReceiveAsync() | |||||
{ | |||||
EndPoint remoteEndPoint = new IPEndPoint(ListenAddress, 0); | |||||
logger.Debug($"++++++Receive Server Port, size:" + _buffer.Length); | |||||
try | |||||
{ | |||||
while (true) | |||||
{ | |||||
var result = await _remote.ReceiveFromAsync(_buffer, SocketFlags.None, remoteEndPoint); | |||||
int bytesRead = result.ReceivedBytes; | |||||
using IMemoryOwner<byte> owner = pool.Rent(bytesRead + 3); | |||||
Memory<byte> o = owner.Memory; | |||||
IEncryptor encryptor = EncryptorFactory.GetEncryptor(_server.method, _server.password); | |||||
int outlen = encryptor.DecryptUDP(o.Span[3..], _buffer.AsSpan(0, bytesRead)); | |||||
logger.Debug(_remoteEndPoint, _localEndPoint, outlen, "UDP Relay down"); | |||||
if (!MemoryMarshal.TryGetArray(o[..(outlen + 3)], out ArraySegment<byte> data)) | |||||
{ | |||||
throw new InvalidOperationException("Can't extract underly array segment"); | |||||
}; | |||||
await _local?.SendToAsync(data, SocketFlags.None, _localEndPoint); | |||||
} | |||||
} | |||||
catch (Exception e) | |||||
{ | |||||
logger.LogUsefulException(e); | |||||
} | |||||
} | |||||
public void Receive() | |||||
{ | |||||
_ = ReceiveAsync(); | |||||
} | |||||
public void Close() | |||||
{ | |||||
try | |||||
{ | |||||
_remote?.Close(); | |||||
} | |||||
catch (ObjectDisposedException) | |||||
{ | |||||
// TODO: handle the ObjectDisposedException | |||||
} | |||||
catch (Exception) | |||||
{ | |||||
// TODO: need more think about handle other Exceptions, or should remove this catch(). | |||||
} | |||||
} | |||||
} | |||||
} | |||||
#region LRU cache | |||||
// cc by-sa 3.0 http://stackoverflow.com/a/3719378/1124054 | |||||
class LRUCache<K, V> where V : UDPRelay.UDPHandler | |||||
{ | |||||
private int capacity; | |||||
private Dictionary<K, LinkedListNode<LRUCacheItem<K, V>>> cacheMap = new Dictionary<K, LinkedListNode<LRUCacheItem<K, V>>>(); | |||||
private LinkedList<LRUCacheItem<K, V>> lruList = new LinkedList<LRUCacheItem<K, V>>(); | |||||
public LRUCache(int capacity) | |||||
{ | |||||
this.capacity = capacity; | |||||
} | |||||
[MethodImpl(MethodImplOptions.Synchronized)] | |||||
public V get(K key) | |||||
{ | |||||
LinkedListNode<LRUCacheItem<K, V>> node; | |||||
if (cacheMap.TryGetValue(key, out node)) | |||||
{ | |||||
V value = node.Value.value; | |||||
lruList.Remove(node); | |||||
lruList.AddLast(node); | |||||
return value; | |||||
} | |||||
return default(V); | |||||
} | |||||
[MethodImpl(MethodImplOptions.Synchronized)] | |||||
public void add(K key, V val) | |||||
{ | |||||
if (cacheMap.Count >= capacity) | |||||
{ | |||||
RemoveFirst(); | |||||
} | |||||
LRUCacheItem<K, V> cacheItem = new LRUCacheItem<K, V>(key, val); | |||||
LinkedListNode<LRUCacheItem<K, V>> node = new LinkedListNode<LRUCacheItem<K, V>>(cacheItem); | |||||
lruList.AddLast(node); | |||||
cacheMap.Add(key, node); | |||||
} | |||||
private void RemoveFirst() | |||||
{ | |||||
// Remove from LRUPriority | |||||
LinkedListNode<LRUCacheItem<K, V>> node = lruList.First; | |||||
lruList.RemoveFirst(); | |||||
// Remove from cache | |||||
cacheMap.Remove(node.Value.key); | |||||
node.Value.value.Close(); | |||||
} | |||||
} | |||||
class LRUCacheItem<K, V> | |||||
{ | |||||
public LRUCacheItem(K k, V v) | |||||
{ | |||||
key = k; | |||||
value = v; | |||||
} | |||||
public K key; | |||||
public V value; | |||||
} | |||||
#endregion | |||||
} | |||||
using System; | |||||
using System.Buffers; | |||||
using System.Collections.Generic; | |||||
using System.Net; | |||||
using System.Net.Sockets; | |||||
using System.Runtime.CompilerServices; | |||||
using System.Runtime.InteropServices; | |||||
using System.Threading.Tasks; | |||||
using NLog; | |||||
using Shadowsocks.Net.Crypto; | |||||
namespace Shadowsocks.Controller | |||||
{ | |||||
class UDPRelay : DatagramService | |||||
{ | |||||
private ShadowsocksController _controller; | |||||
// TODO: choose a smart number | |||||
private LRUCache<IPEndPoint, UDPHandler> _cache = new LRUCache<IPEndPoint, UDPHandler>(512); | |||||
public long outbound = 0; | |||||
public long inbound = 0; | |||||
public UDPRelay(ShadowsocksController controller) | |||||
{ | |||||
this._controller = controller; | |||||
} | |||||
public override async Task<bool> Handle(Memory<byte> packet, Socket socket, EndPoint client) | |||||
{ | |||||
if (socket.ProtocolType != ProtocolType.Udp) | |||||
{ | |||||
return false; | |||||
} | |||||
if (packet.Length < 4) | |||||
{ | |||||
return false; | |||||
} | |||||
IPEndPoint remoteEndPoint = (IPEndPoint)client; | |||||
UDPHandler handler = _cache.get(remoteEndPoint); | |||||
if (handler == null) | |||||
{ | |||||
handler = new UDPHandler(socket, _controller.GetAServer(IStrategyCallerType.UDP, remoteEndPoint, null/*TODO: fix this*/), remoteEndPoint); | |||||
handler.Receive(); | |||||
_cache.add(remoteEndPoint, handler); | |||||
} | |||||
await handler.SendAsync(packet); | |||||
return true; | |||||
} | |||||
public class UDPHandler | |||||
{ | |||||
private static Logger logger = LogManager.GetCurrentClassLogger(); | |||||
private static MemoryPool<byte> pool = MemoryPool<byte>.Shared; | |||||
private Socket _local; | |||||
private Socket _remote; | |||||
private Server _server; | |||||
private byte[] _buffer = new byte[65536]; | |||||
private IPEndPoint _localEndPoint; | |||||
private IPEndPoint _remoteEndPoint; | |||||
private IPAddress ListenAddress | |||||
{ | |||||
get | |||||
{ | |||||
return _remote.AddressFamily switch | |||||
{ | |||||
AddressFamily.InterNetwork => IPAddress.Any, | |||||
AddressFamily.InterNetworkV6 => IPAddress.IPv6Any, | |||||
_ => throw new NotSupportedException(), | |||||
}; | |||||
} | |||||
} | |||||
public UDPHandler(Socket local, Server server, IPEndPoint localEndPoint) | |||||
{ | |||||
_local = local; | |||||
_server = server; | |||||
_localEndPoint = localEndPoint; | |||||
// TODO async resolving | |||||
bool parsed = IPAddress.TryParse(server.server, out IPAddress ipAddress); | |||||
if (!parsed) | |||||
{ | |||||
IPHostEntry ipHostInfo = Dns.GetHostEntry(server.server); | |||||
ipAddress = ipHostInfo.AddressList[0]; | |||||
} | |||||
_remoteEndPoint = new IPEndPoint(ipAddress, server.server_port); | |||||
_remote = new Socket(_remoteEndPoint.AddressFamily, SocketType.Dgram, ProtocolType.Udp); | |||||
_remote.Bind(new IPEndPoint(ListenAddress, 0)); | |||||
} | |||||
public async Task SendAsync(ReadOnlyMemory<byte> data) | |||||
{ | |||||
IEncryptor encryptor = EncryptorFactory.GetEncryptor(_server.method, _server.password); | |||||
using IMemoryOwner<byte> mem = pool.Rent(data.Length + 1000); | |||||
// byte[] dataOut = new byte[slicedData.Length + 1000]; | |||||
int outlen = encryptor.EncryptUDP(data.Span[3..], mem.Memory.Span); | |||||
logger.Debug(_localEndPoint, _remoteEndPoint, outlen, "UDP Relay up"); | |||||
if (!MemoryMarshal.TryGetArray(mem.Memory[..outlen], out ArraySegment<byte> outData)) | |||||
{ | |||||
throw new InvalidOperationException("Can't extract underly array segment"); | |||||
}; | |||||
await _remote?.SendToAsync(outData, SocketFlags.None, _remoteEndPoint); | |||||
} | |||||
public async Task ReceiveAsync() | |||||
{ | |||||
EndPoint remoteEndPoint = new IPEndPoint(ListenAddress, 0); | |||||
logger.Debug($"++++++Receive Server Port, size:" + _buffer.Length); | |||||
try | |||||
{ | |||||
while (true) | |||||
{ | |||||
var result = await _remote.ReceiveFromAsync(_buffer, SocketFlags.None, remoteEndPoint); | |||||
int bytesRead = result.ReceivedBytes; | |||||
using IMemoryOwner<byte> owner = pool.Rent(bytesRead + 3); | |||||
Memory<byte> o = owner.Memory; | |||||
IEncryptor encryptor = EncryptorFactory.GetEncryptor(_server.method, _server.password); | |||||
int outlen = encryptor.DecryptUDP(o.Span[3..], _buffer.AsSpan(0, bytesRead)); | |||||
logger.Debug(_remoteEndPoint, _localEndPoint, outlen, "UDP Relay down"); | |||||
if (!MemoryMarshal.TryGetArray(o[..(outlen + 3)], out ArraySegment<byte> data)) | |||||
{ | |||||
throw new InvalidOperationException("Can't extract underly array segment"); | |||||
}; | |||||
await _local?.SendToAsync(data, SocketFlags.None, _localEndPoint); | |||||
} | |||||
} | |||||
catch (Exception e) | |||||
{ | |||||
logger.LogUsefulException(e); | |||||
} | |||||
} | |||||
public void Receive() | |||||
{ | |||||
_ = ReceiveAsync(); | |||||
} | |||||
public void Close() | |||||
{ | |||||
try | |||||
{ | |||||
_remote?.Close(); | |||||
} | |||||
catch (ObjectDisposedException) | |||||
{ | |||||
// TODO: handle the ObjectDisposedException | |||||
} | |||||
catch (Exception) | |||||
{ | |||||
// TODO: need more think about handle other Exceptions, or should remove this catch(). | |||||
} | |||||
} | |||||
} | |||||
} | |||||
#region LRU cache | |||||
// cc by-sa 3.0 http://stackoverflow.com/a/3719378/1124054 | |||||
class LRUCache<K, V> where V : UDPRelay.UDPHandler | |||||
{ | |||||
private int capacity; | |||||
private Dictionary<K, LinkedListNode<LRUCacheItem<K, V>>> cacheMap = new Dictionary<K, LinkedListNode<LRUCacheItem<K, V>>>(); | |||||
private LinkedList<LRUCacheItem<K, V>> lruList = new LinkedList<LRUCacheItem<K, V>>(); | |||||
public LRUCache(int capacity) | |||||
{ | |||||
this.capacity = capacity; | |||||
} | |||||
[MethodImpl(MethodImplOptions.Synchronized)] | |||||
public V get(K key) | |||||
{ | |||||
LinkedListNode<LRUCacheItem<K, V>> node; | |||||
if (cacheMap.TryGetValue(key, out node)) | |||||
{ | |||||
V value = node.Value.value; | |||||
lruList.Remove(node); | |||||
lruList.AddLast(node); | |||||
return value; | |||||
} | |||||
return default(V); | |||||
} | |||||
[MethodImpl(MethodImplOptions.Synchronized)] | |||||
public void add(K key, V val) | |||||
{ | |||||
if (cacheMap.Count >= capacity) | |||||
{ | |||||
RemoveFirst(); | |||||
} | |||||
LRUCacheItem<K, V> cacheItem = new LRUCacheItem<K, V>(key, val); | |||||
LinkedListNode<LRUCacheItem<K, V>> node = new LinkedListNode<LRUCacheItem<K, V>>(cacheItem); | |||||
lruList.AddLast(node); | |||||
cacheMap.Add(key, node); | |||||
} | |||||
private void RemoveFirst() | |||||
{ | |||||
// Remove from LRUPriority | |||||
LinkedListNode<LRUCacheItem<K, V>> node = lruList.First; | |||||
lruList.RemoveFirst(); | |||||
// Remove from cache | |||||
cacheMap.Remove(node.Value.key); | |||||
node.Value.value.Close(); | |||||
} | |||||
} | |||||
class LRUCacheItem<K, V> | |||||
{ | |||||
public LRUCacheItem(K k, V v) | |||||
{ | |||||
key = k; | |||||
value = v; | |||||
} | |||||
public K key; | |||||
public V value; | |||||
} | |||||
#endregion | |||||
} |
@@ -1,4 +1,4 @@ | |||||
using NLog; | |||||
using NLog; | |||||
using Shadowsocks.Properties; | using Shadowsocks.Properties; | ||||
using Shadowsocks.Util; | using Shadowsocks.Util; | ||||
using System; | using System; | ||||
@@ -13,7 +13,7 @@ using System.Net.Http; | |||||
using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
using System.Security.Cryptography; | using System.Security.Cryptography; | ||||
namespace Shadowsocks.Controller | |||||
namespace Shadowsocks.PAC | |||||
{ | { | ||||
public class GeositeResultEventArgs : EventArgs | public class GeositeResultEventArgs : EventArgs | ||||
{ | { | ||||
@@ -21,7 +21,7 @@ namespace Shadowsocks.Controller | |||||
public GeositeResultEventArgs(bool success) | public GeositeResultEventArgs(bool success) | ||||
{ | { | ||||
this.Success = success; | |||||
Success = success; | |||||
} | } | ||||
} | } | ||||
@@ -1,133 +1,133 @@ | |||||
using NLog; | |||||
using Shadowsocks.Model; | |||||
using Shadowsocks.Properties; | |||||
using Shadowsocks.Util; | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.IO; | |||||
using System.Linq; | |||||
using System.Text; | |||||
using System.Threading.Tasks; | |||||
namespace Shadowsocks.Controller | |||||
{ | |||||
/// <summary> | |||||
/// Processing the PAC file content | |||||
/// </summary> | |||||
public class PACDaemon | |||||
{ | |||||
private static Logger logger = LogManager.GetCurrentClassLogger(); | |||||
public const string PAC_FILE = "pac.txt"; | |||||
public const string USER_RULE_FILE = "user-rule.txt"; | |||||
public const string USER_ABP_FILE = "abp.txt"; | |||||
private Configuration config; | |||||
FileSystemWatcher PACFileWatcher; | |||||
FileSystemWatcher UserRuleFileWatcher; | |||||
public event EventHandler PACFileChanged; | |||||
public event EventHandler UserRuleFileChanged; | |||||
public PACDaemon(Configuration config) | |||||
{ | |||||
this.config = config; | |||||
TouchPACFile(); | |||||
TouchUserRuleFile(); | |||||
this.WatchPacFile(); | |||||
this.WatchUserRuleFile(); | |||||
} | |||||
public string TouchPACFile() | |||||
{ | |||||
if (!File.Exists(PAC_FILE)) | |||||
{ | |||||
GeositeUpdater.MergeAndWritePACFile(config.geositeDirectGroups, config.geositeProxiedGroups, config.geositePreferDirect); | |||||
} | |||||
return PAC_FILE; | |||||
} | |||||
internal string TouchUserRuleFile() | |||||
{ | |||||
if (!File.Exists(USER_RULE_FILE)) | |||||
{ | |||||
File.WriteAllText(USER_RULE_FILE, Resources.user_rule); | |||||
} | |||||
return USER_RULE_FILE; | |||||
} | |||||
internal string GetPACContent() | |||||
{ | |||||
if (!File.Exists(PAC_FILE)) | |||||
{ | |||||
GeositeUpdater.MergeAndWritePACFile(config.geositeDirectGroups, config.geositeProxiedGroups, config.geositePreferDirect); | |||||
} | |||||
return File.ReadAllText(PAC_FILE, Encoding.UTF8); | |||||
} | |||||
private void WatchPacFile() | |||||
{ | |||||
PACFileWatcher?.Dispose(); | |||||
PACFileWatcher = new FileSystemWatcher(Program.WorkingDirectory); | |||||
PACFileWatcher.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName | NotifyFilters.DirectoryName; | |||||
PACFileWatcher.Filter = PAC_FILE; | |||||
PACFileWatcher.Changed += PACFileWatcher_Changed; | |||||
PACFileWatcher.Created += PACFileWatcher_Changed; | |||||
PACFileWatcher.Deleted += PACFileWatcher_Changed; | |||||
PACFileWatcher.Renamed += PACFileWatcher_Changed; | |||||
PACFileWatcher.EnableRaisingEvents = true; | |||||
} | |||||
private void WatchUserRuleFile() | |||||
{ | |||||
UserRuleFileWatcher?.Dispose(); | |||||
UserRuleFileWatcher = new FileSystemWatcher(Program.WorkingDirectory); | |||||
UserRuleFileWatcher.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName | NotifyFilters.DirectoryName; | |||||
UserRuleFileWatcher.Filter = USER_RULE_FILE; | |||||
UserRuleFileWatcher.Changed += UserRuleFileWatcher_Changed; | |||||
UserRuleFileWatcher.Created += UserRuleFileWatcher_Changed; | |||||
UserRuleFileWatcher.Deleted += UserRuleFileWatcher_Changed; | |||||
UserRuleFileWatcher.Renamed += UserRuleFileWatcher_Changed; | |||||
UserRuleFileWatcher.EnableRaisingEvents = true; | |||||
} | |||||
#region FileSystemWatcher.OnChanged() | |||||
// FileSystemWatcher Changed event is raised twice | |||||
// http://stackoverflow.com/questions/1764809/filesystemwatcher-changed-event-is-raised-twice | |||||
// Add a short delay to avoid raise event twice in a short period | |||||
private void PACFileWatcher_Changed(object sender, FileSystemEventArgs e) | |||||
{ | |||||
if (PACFileChanged != null) | |||||
{ | |||||
logger.Info($"Detected: PAC file '{e.Name}' was {e.ChangeType.ToString().ToLower()}."); | |||||
Task.Factory.StartNew(() => | |||||
{ | |||||
((FileSystemWatcher)sender).EnableRaisingEvents = false; | |||||
System.Threading.Thread.Sleep(10); | |||||
PACFileChanged(this, new EventArgs()); | |||||
((FileSystemWatcher)sender).EnableRaisingEvents = true; | |||||
}); | |||||
} | |||||
} | |||||
private void UserRuleFileWatcher_Changed(object sender, FileSystemEventArgs e) | |||||
{ | |||||
if (UserRuleFileChanged != null) | |||||
{ | |||||
logger.Info($"Detected: User Rule file '{e.Name}' was {e.ChangeType.ToString().ToLower()}."); | |||||
Task.Factory.StartNew(() => | |||||
{ | |||||
((FileSystemWatcher)sender).EnableRaisingEvents = false; | |||||
System.Threading.Thread.Sleep(10); | |||||
UserRuleFileChanged(this, new EventArgs()); | |||||
((FileSystemWatcher)sender).EnableRaisingEvents = true; | |||||
}); | |||||
} | |||||
} | |||||
#endregion | |||||
} | |||||
} | |||||
using NLog; | |||||
using Shadowsocks.Model; | |||||
using Shadowsocks.Properties; | |||||
using Shadowsocks.Util; | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.IO; | |||||
using System.Linq; | |||||
using System.Text; | |||||
using System.Threading.Tasks; | |||||
namespace Shadowsocks.PAC | |||||
{ | |||||
/// <summary> | |||||
/// Processing the PAC file content | |||||
/// </summary> | |||||
public class PACDaemon | |||||
{ | |||||
private static Logger logger = LogManager.GetCurrentClassLogger(); | |||||
public const string PAC_FILE = "pac.txt"; | |||||
public const string USER_RULE_FILE = "user-rule.txt"; | |||||
public const string USER_ABP_FILE = "abp.txt"; | |||||
private Configuration config; | |||||
FileSystemWatcher PACFileWatcher; | |||||
FileSystemWatcher UserRuleFileWatcher; | |||||
public event EventHandler PACFileChanged; | |||||
public event EventHandler UserRuleFileChanged; | |||||
public PACDaemon(Configuration config) | |||||
{ | |||||
this.config = config; | |||||
TouchPACFile(); | |||||
TouchUserRuleFile(); | |||||
this.WatchPacFile(); | |||||
this.WatchUserRuleFile(); | |||||
} | |||||
public string TouchPACFile() | |||||
{ | |||||
if (!File.Exists(PAC_FILE)) | |||||
{ | |||||
GeositeUpdater.MergeAndWritePACFile(config.geositeDirectGroups, config.geositeProxiedGroups, config.geositePreferDirect); | |||||
} | |||||
return PAC_FILE; | |||||
} | |||||
internal string TouchUserRuleFile() | |||||
{ | |||||
if (!File.Exists(USER_RULE_FILE)) | |||||
{ | |||||
File.WriteAllText(USER_RULE_FILE, Resources.user_rule); | |||||
} | |||||
return USER_RULE_FILE; | |||||
} | |||||
internal string GetPACContent() | |||||
{ | |||||
if (!File.Exists(PAC_FILE)) | |||||
{ | |||||
GeositeUpdater.MergeAndWritePACFile(config.geositeDirectGroups, config.geositeProxiedGroups, config.geositePreferDirect); | |||||
} | |||||
return File.ReadAllText(PAC_FILE, Encoding.UTF8); | |||||
} | |||||
private void WatchPacFile() | |||||
{ | |||||
PACFileWatcher?.Dispose(); | |||||
PACFileWatcher = new FileSystemWatcher(Program.WorkingDirectory); | |||||
PACFileWatcher.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName | NotifyFilters.DirectoryName; | |||||
PACFileWatcher.Filter = PAC_FILE; | |||||
PACFileWatcher.Changed += PACFileWatcher_Changed; | |||||
PACFileWatcher.Created += PACFileWatcher_Changed; | |||||
PACFileWatcher.Deleted += PACFileWatcher_Changed; | |||||
PACFileWatcher.Renamed += PACFileWatcher_Changed; | |||||
PACFileWatcher.EnableRaisingEvents = true; | |||||
} | |||||
private void WatchUserRuleFile() | |||||
{ | |||||
UserRuleFileWatcher?.Dispose(); | |||||
UserRuleFileWatcher = new FileSystemWatcher(Program.WorkingDirectory); | |||||
UserRuleFileWatcher.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName | NotifyFilters.DirectoryName; | |||||
UserRuleFileWatcher.Filter = USER_RULE_FILE; | |||||
UserRuleFileWatcher.Changed += UserRuleFileWatcher_Changed; | |||||
UserRuleFileWatcher.Created += UserRuleFileWatcher_Changed; | |||||
UserRuleFileWatcher.Deleted += UserRuleFileWatcher_Changed; | |||||
UserRuleFileWatcher.Renamed += UserRuleFileWatcher_Changed; | |||||
UserRuleFileWatcher.EnableRaisingEvents = true; | |||||
} | |||||
#region FileSystemWatcher.OnChanged() | |||||
// FileSystemWatcher Changed event is raised twice | |||||
// http://stackoverflow.com/questions/1764809/filesystemwatcher-changed-event-is-raised-twice | |||||
// Add a short delay to avoid raise event twice in a short period | |||||
private void PACFileWatcher_Changed(object sender, FileSystemEventArgs e) | |||||
{ | |||||
if (PACFileChanged != null) | |||||
{ | |||||
logger.Info($"Detected: PAC file '{e.Name}' was {e.ChangeType.ToString().ToLower()}."); | |||||
Task.Factory.StartNew(() => | |||||
{ | |||||
((FileSystemWatcher)sender).EnableRaisingEvents = false; | |||||
System.Threading.Thread.Sleep(10); | |||||
PACFileChanged(this, new EventArgs()); | |||||
((FileSystemWatcher)sender).EnableRaisingEvents = true; | |||||
}); | |||||
} | |||||
} | |||||
private void UserRuleFileWatcher_Changed(object sender, FileSystemEventArgs e) | |||||
{ | |||||
if (UserRuleFileChanged != null) | |||||
{ | |||||
logger.Info($"Detected: User Rule file '{e.Name}' was {e.ChangeType.ToString().ToLower()}."); | |||||
Task.Factory.StartNew(() => | |||||
{ | |||||
((FileSystemWatcher)sender).EnableRaisingEvents = false; | |||||
System.Threading.Thread.Sleep(10); | |||||
UserRuleFileChanged(this, new EventArgs()); | |||||
((FileSystemWatcher)sender).EnableRaisingEvents = true; | |||||
}); | |||||
} | |||||
} | |||||
#endregion | |||||
} | |||||
} |
@@ -1,210 +1,209 @@ | |||||
using Shadowsocks.Encryption; | |||||
using Shadowsocks.Model; | |||||
using Shadowsocks.Util; | |||||
using System; | |||||
using System.Net; | |||||
using System.Net.Sockets; | |||||
using System.Text; | |||||
using System.Web; | |||||
using NLog; | |||||
namespace Shadowsocks.Controller | |||||
{ | |||||
public class PACServer : StreamService | |||||
{ | |||||
private static Logger logger = LogManager.GetCurrentClassLogger(); | |||||
public const string RESOURCE_NAME = "pac"; | |||||
private string PacSecret | |||||
{ | |||||
get | |||||
{ | |||||
if (string.IsNullOrEmpty(_cachedPacSecret)) | |||||
{ | |||||
_cachedPacSecret = HttpServerUtilityUrlToken.Encode(RNG.GetBytes(32)); | |||||
} | |||||
return _cachedPacSecret; | |||||
} | |||||
} | |||||
private string _cachedPacSecret = ""; | |||||
public string PacUrl { get; private set; } = ""; | |||||
private Configuration _config; | |||||
private PACDaemon _pacDaemon; | |||||
public PACServer(PACDaemon pacDaemon) | |||||
{ | |||||
_pacDaemon = pacDaemon; | |||||
} | |||||
public void UpdatePACURL(Configuration config) | |||||
{ | |||||
_config = config; | |||||
string usedSecret = _config.secureLocalPac ? $"&secret={PacSecret}" : ""; | |||||
string contentHash = GetHash(_pacDaemon.GetPACContent()); | |||||
PacUrl = $"http://{config.LocalHost}:{config.localPort}/{RESOURCE_NAME}?hash={contentHash}{usedSecret}"; | |||||
logger.Debug("Set PAC URL:" + PacUrl); | |||||
} | |||||
private static string GetHash(string content) | |||||
{ | |||||
return HttpServerUtilityUrlToken.Encode(CryptoUtils.MD5(Encoding.ASCII.GetBytes(content))); | |||||
} | |||||
public override bool Handle(CachedNetworkStream stream, object state) | |||||
{ | |||||
byte[] fp = new byte[256]; | |||||
int len = stream.ReadFirstBlock(fp); | |||||
return Handle(fp, len, stream.Socket, state); | |||||
} | |||||
[Obsolete] | |||||
public override bool Handle(byte[] firstPacket, int length, Socket socket, object state) | |||||
{ | |||||
if (socket.ProtocolType != ProtocolType.Tcp) | |||||
{ | |||||
return false; | |||||
} | |||||
try | |||||
{ | |||||
/* | |||||
* RFC 7230 | |||||
* | |||||
GET /hello.txt HTTP/1.1 | |||||
User-Agent: curl/7.16.3 libcurl/7.16.3 OpenSSL/0.9.7l zlib/1.2.3 | |||||
Host: www.example.com | |||||
Accept-Language: en, mi | |||||
*/ | |||||
string request = Encoding.UTF8.GetString(firstPacket, 0, length); | |||||
string[] lines = request.Split('\r', '\n'); | |||||
bool hostMatch = false, pathMatch = false, useSocks = false; | |||||
bool secretMatch = !_config.secureLocalPac; | |||||
if (lines.Length < 2) // need at lease RequestLine + Host | |||||
{ | |||||
return false; | |||||
} | |||||
// parse request line | |||||
string requestLine = lines[0]; | |||||
// GET /pac?t=yyyyMMddHHmmssfff&secret=foobar HTTP/1.1 | |||||
string[] requestItems = requestLine.Split(' '); | |||||
if (requestItems.Length == 3 && requestItems[0] == "GET") | |||||
{ | |||||
int index = requestItems[1].IndexOf('?'); | |||||
if (index < 0) | |||||
{ | |||||
index = requestItems[1].Length; | |||||
} | |||||
string resourceString = requestItems[1].Substring(0, index).Remove(0, 1); | |||||
if (string.Equals(resourceString, RESOURCE_NAME, StringComparison.OrdinalIgnoreCase)) | |||||
{ | |||||
pathMatch = true; | |||||
if (!secretMatch) | |||||
{ | |||||
string queryString = requestItems[1].Substring(index); | |||||
if (queryString.Contains(PacSecret)) | |||||
{ | |||||
secretMatch = true; | |||||
} | |||||
} | |||||
} | |||||
} | |||||
// parse request header | |||||
for (int i = 1; i < lines.Length; i++) | |||||
{ | |||||
if (string.IsNullOrEmpty(lines[i])) | |||||
continue; | |||||
string[] kv = lines[i].Split(new char[] { ':' }, 2); | |||||
if (kv.Length == 2) | |||||
{ | |||||
if (kv[0] == "Host") | |||||
{ | |||||
if (kv[1].Trim() == ((IPEndPoint)socket.LocalEndPoint).ToString()) | |||||
{ | |||||
hostMatch = true; | |||||
} | |||||
} | |||||
//else if (kv[0] == "User-Agent") | |||||
//{ | |||||
// // we need to drop connections when changing servers | |||||
// if (kv[1].IndexOf("Chrome") >= 0) | |||||
// { | |||||
// useSocks = true; | |||||
// } | |||||
//} | |||||
} | |||||
} | |||||
if (hostMatch && pathMatch) | |||||
{ | |||||
if (!secretMatch) | |||||
{ | |||||
socket.Close(); // Close immediately | |||||
} | |||||
else | |||||
{ | |||||
SendResponse(socket, useSocks); | |||||
} | |||||
return true; | |||||
} | |||||
return false; | |||||
} | |||||
catch (ArgumentException) | |||||
{ | |||||
return false; | |||||
} | |||||
} | |||||
public void SendResponse(Socket socket, bool useSocks) | |||||
{ | |||||
try | |||||
{ | |||||
IPEndPoint localEndPoint = (IPEndPoint)socket.LocalEndPoint; | |||||
string proxy = GetPACAddress(localEndPoint, useSocks); | |||||
string pacContent = $"var __PROXY__ = '{proxy}';\n" + _pacDaemon.GetPACContent(); | |||||
string responseHead = | |||||
$@"HTTP/1.1 200 OK | |||||
Server: ShadowsocksWindows/{UpdateChecker.Version} | |||||
Content-Type: application/x-ns-proxy-autoconfig | |||||
Content-Length: { Encoding.UTF8.GetBytes(pacContent).Length} | |||||
Connection: Close | |||||
"; | |||||
byte[] response = Encoding.UTF8.GetBytes(responseHead + pacContent); | |||||
socket.BeginSend(response, 0, response.Length, 0, new AsyncCallback(SendCallback), socket); | |||||
} | |||||
catch (Exception e) | |||||
{ | |||||
logger.LogUsefulException(e); | |||||
socket.Close(); | |||||
} | |||||
} | |||||
private void SendCallback(IAsyncResult ar) | |||||
{ | |||||
Socket conn = (Socket)ar.AsyncState; | |||||
try | |||||
{ | |||||
conn.Shutdown(SocketShutdown.Send); | |||||
} | |||||
catch | |||||
{ } | |||||
} | |||||
private string GetPACAddress(IPEndPoint localEndPoint, bool useSocks) | |||||
{ | |||||
return localEndPoint.AddressFamily == AddressFamily.InterNetworkV6 | |||||
? $"{(useSocks ? "SOCKS5" : "PROXY")} [{localEndPoint.Address}]:{_config.localPort};" | |||||
: $"{(useSocks ? "SOCKS5" : "PROXY")} {localEndPoint.Address}:{_config.localPort};"; | |||||
} | |||||
} | |||||
} | |||||
using System; | |||||
using System.Net; | |||||
using System.Net.Sockets; | |||||
using System.Text; | |||||
using NLog; | |||||
using Shadowsocks.Net; | |||||
using Shadowsocks.Utilities; | |||||
using Shadowsocks.Net.Crypto; | |||||
namespace Shadowsocks.PAC | |||||
{ | |||||
public class PACServer : StreamService | |||||
{ | |||||
private static Logger logger = LogManager.GetCurrentClassLogger(); | |||||
public const string RESOURCE_NAME = "pac"; | |||||
private string PacSecret | |||||
{ | |||||
get | |||||
{ | |||||
if (string.IsNullOrEmpty(_cachedPacSecret)) | |||||
{ | |||||
_cachedPacSecret = Base64Url.Encode(RNG.GetBytes(32)); | |||||
} | |||||
return _cachedPacSecret; | |||||
} | |||||
} | |||||
private string _cachedPacSecret = ""; | |||||
public string PacUrl { get; private set; } = ""; | |||||
private Configuration _config; | |||||
private PACDaemon _pacDaemon; | |||||
public PACServer(PACDaemon pacDaemon) | |||||
{ | |||||
_pacDaemon = pacDaemon; | |||||
} | |||||
public void UpdatePACURL(Configuration config) | |||||
{ | |||||
_config = config; | |||||
string usedSecret = _config.secureLocalPac ? $"&secret={PacSecret}" : ""; | |||||
string contentHash = GetHash(_pacDaemon.GetPACContent()); | |||||
PacUrl = $"http://{config.LocalHost}:{config.localPort}/{RESOURCE_NAME}?hash={contentHash}{usedSecret}"; | |||||
logger.Debug("Set PAC URL:" + PacUrl); | |||||
} | |||||
private static string GetHash(string content) | |||||
{ | |||||
return Base64Url.Encode(CryptoUtils.MD5(Encoding.ASCII.GetBytes(content))); | |||||
} | |||||
public override bool Handle(CachedNetworkStream stream, object state) | |||||
{ | |||||
byte[] fp = new byte[256]; | |||||
int len = stream.ReadFirstBlock(fp); | |||||
return Handle(fp, len, stream.Socket, state); | |||||
} | |||||
[Obsolete] | |||||
public override bool Handle(byte[] firstPacket, int length, Socket socket, object state) | |||||
{ | |||||
if (socket.ProtocolType != ProtocolType.Tcp) | |||||
{ | |||||
return false; | |||||
} | |||||
try | |||||
{ | |||||
/* | |||||
* RFC 7230 | |||||
* | |||||
GET /hello.txt HTTP/1.1 | |||||
User-Agent: curl/7.16.3 libcurl/7.16.3 OpenSSL/0.9.7l zlib/1.2.3 | |||||
Host: www.example.com | |||||
Accept-Language: en, mi | |||||
*/ | |||||
string request = Encoding.UTF8.GetString(firstPacket, 0, length); | |||||
string[] lines = request.Split('\r', '\n'); | |||||
bool hostMatch = false, pathMatch = false, useSocks = false; | |||||
bool secretMatch = !_config.secureLocalPac; | |||||
if (lines.Length < 2) // need at lease RequestLine + Host | |||||
{ | |||||
return false; | |||||
} | |||||
// parse request line | |||||
string requestLine = lines[0]; | |||||
// GET /pac?t=yyyyMMddHHmmssfff&secret=foobar HTTP/1.1 | |||||
string[] requestItems = requestLine.Split(' '); | |||||
if (requestItems.Length == 3 && requestItems[0] == "GET") | |||||
{ | |||||
int index = requestItems[1].IndexOf('?'); | |||||
if (index < 0) | |||||
{ | |||||
index = requestItems[1].Length; | |||||
} | |||||
string resourceString = requestItems[1].Substring(0, index).Remove(0, 1); | |||||
if (string.Equals(resourceString, RESOURCE_NAME, StringComparison.OrdinalIgnoreCase)) | |||||
{ | |||||
pathMatch = true; | |||||
if (!secretMatch) | |||||
{ | |||||
string queryString = requestItems[1].Substring(index); | |||||
if (queryString.Contains(PacSecret)) | |||||
{ | |||||
secretMatch = true; | |||||
} | |||||
} | |||||
} | |||||
} | |||||
// parse request header | |||||
for (int i = 1; i < lines.Length; i++) | |||||
{ | |||||
if (string.IsNullOrEmpty(lines[i])) | |||||
continue; | |||||
string[] kv = lines[i].Split(new char[] { ':' }, 2); | |||||
if (kv.Length == 2) | |||||
{ | |||||
if (kv[0] == "Host") | |||||
{ | |||||
if (kv[1].Trim() == ((IPEndPoint)socket.LocalEndPoint).ToString()) | |||||
{ | |||||
hostMatch = true; | |||||
} | |||||
} | |||||
//else if (kv[0] == "User-Agent") | |||||
//{ | |||||
// // we need to drop connections when changing servers | |||||
// if (kv[1].IndexOf("Chrome") >= 0) | |||||
// { | |||||
// useSocks = true; | |||||
// } | |||||
//} | |||||
} | |||||
} | |||||
if (hostMatch && pathMatch) | |||||
{ | |||||
if (!secretMatch) | |||||
{ | |||||
socket.Close(); // Close immediately | |||||
} | |||||
else | |||||
{ | |||||
SendResponse(socket, useSocks); | |||||
} | |||||
return true; | |||||
} | |||||
return false; | |||||
} | |||||
catch (ArgumentException) | |||||
{ | |||||
return false; | |||||
} | |||||
} | |||||
public void SendResponse(Socket socket, bool useSocks) | |||||
{ | |||||
try | |||||
{ | |||||
IPEndPoint localEndPoint = (IPEndPoint)socket.LocalEndPoint; | |||||
string proxy = GetPACAddress(localEndPoint, useSocks); | |||||
string pacContent = $"var __PROXY__ = '{proxy}';\n" + _pacDaemon.GetPACContent(); | |||||
string responseHead = | |||||
$@"HTTP/1.1 200 OK | |||||
Server: ShadowsocksWindows/{UpdateChecker.Version} | |||||
Content-Type: application/x-ns-proxy-autoconfig | |||||
Content-Length: { Encoding.UTF8.GetBytes(pacContent).Length} | |||||
Connection: Close | |||||
"; | |||||
byte[] response = Encoding.UTF8.GetBytes(responseHead + pacContent); | |||||
socket.BeginSend(response, 0, response.Length, 0, new AsyncCallback(SendCallback), socket); | |||||
} | |||||
catch (Exception e) | |||||
{ | |||||
logger.LogUsefulException(e); | |||||
socket.Close(); | |||||
} | |||||
} | |||||
private void SendCallback(IAsyncResult ar) | |||||
{ | |||||
Socket conn = (Socket)ar.AsyncState; | |||||
try | |||||
{ | |||||
conn.Shutdown(SocketShutdown.Send); | |||||
} | |||||
catch | |||||
{ } | |||||
} | |||||
private string GetPACAddress(IPEndPoint localEndPoint, bool useSocks) | |||||
{ | |||||
return localEndPoint.AddressFamily == AddressFamily.InterNetworkV6 | |||||
? $"{(useSocks ? "SOCKS5" : "PROXY")} [{localEndPoint.Address}]:{_config.localPort};" | |||||
: $"{(useSocks ? "SOCKS5" : "PROXY")} {localEndPoint.Address}:{_config.localPort};"; | |||||
} | |||||
} | |||||
} |
@@ -0,0 +1,12 @@ | |||||
<Project Sdk="Microsoft.NET.Sdk"> | |||||
<PropertyGroup> | |||||
<TargetFramework>netcoreapp3.1</TargetFramework> | |||||
</PropertyGroup> | |||||
<ItemGroup> | |||||
<ProjectReference Include="..\Shadowsocks.Net\Shadowsocks.Net.csproj" /> | |||||
<ProjectReference Include="..\Shadowsocks\Shadowsocks.csproj" /> | |||||
</ItemGroup> | |||||
</Project> |
@@ -0,0 +1,11 @@ | |||||
<Project Sdk="Microsoft.NET.Sdk"> | |||||
<PropertyGroup> | |||||
<TargetFramework>netcoreapp3.1</TargetFramework> | |||||
</PropertyGroup> | |||||
<ItemGroup> | |||||
<PackageReference Include="Google.Protobuf" Version="3.13.0" /> | |||||
</ItemGroup> | |||||
</Project> |
@@ -2,12 +2,12 @@ | |||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" | xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" | ||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" | xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" | ||||
xmlns:local="clr-namespace:Shadowsocks.WPF" | xmlns:local="clr-namespace:Shadowsocks.WPF" | ||||
xmlns:md="http://materialdesigninxaml.net/winfx/xaml/themes" | |||||
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes" | |||||
StartupUri="Views/MainWindow.xaml"> | StartupUri="Views/MainWindow.xaml"> | ||||
<Application.Resources> | <Application.Resources> | ||||
<ResourceDictionary> | <ResourceDictionary> | ||||
<ResourceDictionary.MergedDictionaries> | <ResourceDictionary.MergedDictionaries> | ||||
<md:BundledTheme BaseTheme="Light" PrimaryColor="DeepPurple" SecondaryColor="Lime" /> | |||||
<materialDesign:CustomColorTheme BaseTheme="Inherit" PrimaryColor="#3d5afe" SecondaryColor="#00c853" /> | |||||
<ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.Defaults.xaml" /> | <ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.Defaults.xaml" /> | ||||
</ResourceDictionary.MergedDictionaries> | </ResourceDictionary.MergedDictionaries> | ||||
</ResourceDictionary> | </ResourceDictionary> | ||||
@@ -1,153 +1,153 @@ | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.Linq; | |||||
using System.Runtime.InteropServices; | |||||
using Microsoft.Win32; | |||||
using NLog; | |||||
using Shadowsocks.Util; | |||||
namespace Shadowsocks.Controller | |||||
{ | |||||
static class AutoStartup | |||||
{ | |||||
private static Logger logger = LogManager.GetCurrentClassLogger(); | |||||
// Don't use Application.ExecutablePath | |||||
// see https://stackoverflow.com/questions/12945805/odd-c-sharp-path-issue | |||||
private static string Key = "Shadowsocks_" + Program.ExecutablePath.GetHashCode(); | |||||
public static bool Set(bool enabled) | |||||
{ | |||||
RegistryKey runKey = null; | |||||
try | |||||
{ | |||||
runKey = Utils.OpenRegKey(@"Software\Microsoft\Windows\CurrentVersion\Run", true); | |||||
if (runKey == null) | |||||
{ | |||||
logger.Error(@"Cannot find HKCU\Software\Microsoft\Windows\CurrentVersion\Run"); | |||||
return false; | |||||
} | |||||
if (enabled) | |||||
{ | |||||
runKey.SetValue(Key, Program.ExecutablePath); | |||||
} | |||||
else | |||||
{ | |||||
runKey.DeleteValue(Key); | |||||
} | |||||
// When autostartup setting change, change RegisterForRestart state to avoid start 2 times | |||||
RegisterForRestart(!enabled); | |||||
return true; | |||||
} | |||||
catch (Exception e) | |||||
{ | |||||
logger.LogUsefulException(e); | |||||
return false; | |||||
} | |||||
finally | |||||
{ | |||||
if (runKey != null) | |||||
{ | |||||
try | |||||
{ | |||||
runKey.Close(); | |||||
runKey.Dispose(); | |||||
} | |||||
catch (Exception e) | |||||
{ logger.LogUsefulException(e); } | |||||
} | |||||
} | |||||
} | |||||
public static bool Check() | |||||
{ | |||||
RegistryKey runKey = null; | |||||
try | |||||
{ | |||||
runKey = Utils.OpenRegKey(@"Software\Microsoft\Windows\CurrentVersion\Run", true); | |||||
if (runKey == null) | |||||
{ | |||||
logger.Error(@"Cannot find HKCU\Software\Microsoft\Windows\CurrentVersion\Run"); | |||||
return false; | |||||
} | |||||
string[] runList = runKey.GetValueNames(); | |||||
foreach (string item in runList) | |||||
{ | |||||
if (item.Equals(Key, StringComparison.OrdinalIgnoreCase)) | |||||
return true; | |||||
else if (item.Equals("Shadowsocks", StringComparison.OrdinalIgnoreCase)) // Compatibility with older versions | |||||
{ | |||||
string value = Convert.ToString(runKey.GetValue(item)); | |||||
if (Program.ExecutablePath.Equals(value, StringComparison.OrdinalIgnoreCase)) | |||||
{ | |||||
runKey.DeleteValue(item); | |||||
runKey.SetValue(Key, Program.ExecutablePath); | |||||
return true; | |||||
} | |||||
} | |||||
} | |||||
return false; | |||||
} | |||||
catch (Exception e) | |||||
{ | |||||
logger.LogUsefulException(e); | |||||
return false; | |||||
} | |||||
finally | |||||
{ | |||||
if (runKey != null) | |||||
{ | |||||
try | |||||
{ | |||||
runKey.Close(); | |||||
runKey.Dispose(); | |||||
} | |||||
catch (Exception e) | |||||
{ logger.LogUsefulException(e); } | |||||
} | |||||
} | |||||
} | |||||
[DllImport("kernel32.dll", SetLastError = true)] | |||||
static extern int RegisterApplicationRestart([MarshalAs(UnmanagedType.LPWStr)] string commandLineArgs, int Flags); | |||||
[DllImport("kernel32.dll", SetLastError = true)] | |||||
static extern int UnregisterApplicationRestart(); | |||||
[Flags] | |||||
enum ApplicationRestartFlags | |||||
{ | |||||
RESTART_ALWAYS = 0, | |||||
RESTART_NO_CRASH = 1, | |||||
RESTART_NO_HANG = 2, | |||||
RESTART_NO_PATCH = 4, | |||||
RESTART_NO_REBOOT = 8, | |||||
} | |||||
// register restart after system reboot/update | |||||
public static void RegisterForRestart(bool register) | |||||
{ | |||||
// requested register and not autostartup | |||||
if (register && !Check()) | |||||
{ | |||||
// escape command line parameter | |||||
string[] args = new List<string>(Program.Args) | |||||
.Select(p => p.Replace("\"", "\\\"")) // escape " to \" | |||||
.Select(p => p.IndexOf(" ") >= 0 ? "\"" + p + "\"" : p) // encapsule with " | |||||
.ToArray(); | |||||
string cmdline = string.Join(" ", args); | |||||
// first parameter is process command line parameter | |||||
// needn't include the name of the executable in the command line | |||||
RegisterApplicationRestart(cmdline, (int)(ApplicationRestartFlags.RESTART_NO_CRASH | ApplicationRestartFlags.RESTART_NO_HANG)); | |||||
logger.Debug("Register restart after system reboot, command line:" + cmdline); | |||||
} | |||||
// requested unregister, which has no side effect | |||||
else if (!register) | |||||
{ | |||||
UnregisterApplicationRestart(); | |||||
logger.Debug("Unregister restart after system reboot"); | |||||
} | |||||
} | |||||
} | |||||
} | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.Linq; | |||||
using System.Runtime.InteropServices; | |||||
using Microsoft.Win32; | |||||
using NLog; | |||||
using Shadowsocks.Util; | |||||
namespace Shadowsocks.WPF.Behaviors | |||||
{ | |||||
static class AutoStartup | |||||
{ | |||||
private static Logger logger = LogManager.GetCurrentClassLogger(); | |||||
// Don't use Application.ExecutablePath | |||||
// see https://stackoverflow.com/questions/12945805/odd-c-sharp-path-issue | |||||
private static string Key = "Shadowsocks_" + Program.ExecutablePath.GetHashCode(); | |||||
public static bool Set(bool enabled) | |||||
{ | |||||
RegistryKey runKey = null; | |||||
try | |||||
{ | |||||
runKey = Utils.OpenRegKey(@"Software\Microsoft\Windows\CurrentVersion\Run", true); | |||||
if (runKey == null) | |||||
{ | |||||
logger.Error(@"Cannot find HKCU\Software\Microsoft\Windows\CurrentVersion\Run"); | |||||
return false; | |||||
} | |||||
if (enabled) | |||||
{ | |||||
runKey.SetValue(Key, Program.ExecutablePath); | |||||
} | |||||
else | |||||
{ | |||||
runKey.DeleteValue(Key); | |||||
} | |||||
// When autostartup setting change, change RegisterForRestart state to avoid start 2 times | |||||
RegisterForRestart(!enabled); | |||||
return true; | |||||
} | |||||
catch (Exception e) | |||||
{ | |||||
logger.LogUsefulException(e); | |||||
return false; | |||||
} | |||||
finally | |||||
{ | |||||
if (runKey != null) | |||||
{ | |||||
try | |||||
{ | |||||
runKey.Close(); | |||||
runKey.Dispose(); | |||||
} | |||||
catch (Exception e) | |||||
{ logger.LogUsefulException(e); } | |||||
} | |||||
} | |||||
} | |||||
public static bool Check() | |||||
{ | |||||
RegistryKey runKey = null; | |||||
try | |||||
{ | |||||
runKey = Utils.OpenRegKey(@"Software\Microsoft\Windows\CurrentVersion\Run", true); | |||||
if (runKey == null) | |||||
{ | |||||
logger.Error(@"Cannot find HKCU\Software\Microsoft\Windows\CurrentVersion\Run"); | |||||
return false; | |||||
} | |||||
string[] runList = runKey.GetValueNames(); | |||||
foreach (string item in runList) | |||||
{ | |||||
if (item.Equals(Key, StringComparison.OrdinalIgnoreCase)) | |||||
return true; | |||||
else if (item.Equals("Shadowsocks", StringComparison.OrdinalIgnoreCase)) // Compatibility with older versions | |||||
{ | |||||
string value = Convert.ToString(runKey.GetValue(item)); | |||||
if (Program.ExecutablePath.Equals(value, StringComparison.OrdinalIgnoreCase)) | |||||
{ | |||||
runKey.DeleteValue(item); | |||||
runKey.SetValue(Key, Program.ExecutablePath); | |||||
return true; | |||||
} | |||||
} | |||||
} | |||||
return false; | |||||
} | |||||
catch (Exception e) | |||||
{ | |||||
logger.LogUsefulException(e); | |||||
return false; | |||||
} | |||||
finally | |||||
{ | |||||
if (runKey != null) | |||||
{ | |||||
try | |||||
{ | |||||
runKey.Close(); | |||||
runKey.Dispose(); | |||||
} | |||||
catch (Exception e) | |||||
{ logger.LogUsefulException(e); } | |||||
} | |||||
} | |||||
} | |||||
[DllImport("kernel32.dll", SetLastError = true)] | |||||
static extern int RegisterApplicationRestart([MarshalAs(UnmanagedType.LPWStr)] string commandLineArgs, int Flags); | |||||
[DllImport("kernel32.dll", SetLastError = true)] | |||||
static extern int UnregisterApplicationRestart(); | |||||
[Flags] | |||||
enum ApplicationRestartFlags | |||||
{ | |||||
RESTART_ALWAYS = 0, | |||||
RESTART_NO_CRASH = 1, | |||||
RESTART_NO_HANG = 2, | |||||
RESTART_NO_PATCH = 4, | |||||
RESTART_NO_REBOOT = 8, | |||||
} | |||||
// register restart after system reboot/update | |||||
public static void RegisterForRestart(bool register) | |||||
{ | |||||
// requested register and not autostartup | |||||
if (register && !Check()) | |||||
{ | |||||
// escape command line parameter | |||||
string[] args = new List<string>(Program.Args) | |||||
.Select(p => p.Replace("\"", "\\\"")) // escape " to \" | |||||
.Select(p => p.IndexOf(" ") >= 0 ? "\"" + p + "\"" : p) // encapsule with " | |||||
.ToArray(); | |||||
string cmdline = string.Join(" ", args); | |||||
// first parameter is process command line parameter | |||||
// needn't include the name of the executable in the command line | |||||
RegisterApplicationRestart(cmdline, (int)(ApplicationRestartFlags.RESTART_NO_CRASH | ApplicationRestartFlags.RESTART_NO_HANG)); | |||||
logger.Debug("Register restart after system reboot, command line:" + cmdline); | |||||
} | |||||
// requested unregister, which has no side effect | |||||
else if (!register) | |||||
{ | |||||
UnregisterApplicationRestart(); | |||||
logger.Debug("Unregister restart after system reboot"); | |||||
} | |||||
} | |||||
} | |||||
} |
@@ -1,68 +1,68 @@ | |||||
using NLog; | |||||
using System; | |||||
using System.IO; | |||||
using System.IO.Compression; | |||||
using System.Text; | |||||
namespace Shadowsocks.Controller | |||||
{ | |||||
public static class FileManager | |||||
{ | |||||
private static Logger logger = LogManager.GetCurrentClassLogger(); | |||||
public static bool ByteArrayToFile(string fileName, byte[] content) | |||||
{ | |||||
try | |||||
{ | |||||
using (var fs = new FileStream(fileName, FileMode.Create, FileAccess.Write)) | |||||
fs.Write(content, 0, content.Length); | |||||
return true; | |||||
} | |||||
catch (Exception ex) | |||||
{ | |||||
logger.Error(ex); | |||||
} | |||||
return false; | |||||
} | |||||
public static void UncompressFile(string fileName, byte[] content) | |||||
{ | |||||
// Because the uncompressed size of the file is unknown, | |||||
// we are using an arbitrary buffer size. | |||||
byte[] buffer = new byte[4096]; | |||||
int n; | |||||
using(var fs = File.Create(fileName)) | |||||
using (var input = new GZipStream(new MemoryStream(content), | |||||
CompressionMode.Decompress, false)) | |||||
{ | |||||
while ((n = input.Read(buffer, 0, buffer.Length)) > 0) | |||||
{ | |||||
fs.Write(buffer, 0, n); | |||||
} | |||||
} | |||||
} | |||||
public static string NonExclusiveReadAllText(string path) | |||||
{ | |||||
return NonExclusiveReadAllText(path, Encoding.Default); | |||||
} | |||||
public static string NonExclusiveReadAllText(string path, Encoding encoding) | |||||
{ | |||||
try | |||||
{ | |||||
using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) | |||||
using (var sr = new StreamReader(fs, encoding)) | |||||
{ | |||||
return sr.ReadToEnd(); | |||||
} | |||||
} | |||||
catch (Exception ex) | |||||
{ | |||||
logger.Error(ex); | |||||
throw ex; | |||||
} | |||||
} | |||||
} | |||||
} | |||||
using NLog; | |||||
using System; | |||||
using System.IO; | |||||
using System.IO.Compression; | |||||
using System.Text; | |||||
namespace Shadowsocks.WPF.Behaviors | |||||
{ | |||||
public static class FileManager | |||||
{ | |||||
private static Logger logger = LogManager.GetCurrentClassLogger(); | |||||
public static bool ByteArrayToFile(string fileName, byte[] content) | |||||
{ | |||||
try | |||||
{ | |||||
using (var fs = new FileStream(fileName, FileMode.Create, FileAccess.Write)) | |||||
fs.Write(content, 0, content.Length); | |||||
return true; | |||||
} | |||||
catch (Exception ex) | |||||
{ | |||||
logger.Error(ex); | |||||
} | |||||
return false; | |||||
} | |||||
public static void UncompressFile(string fileName, byte[] content) | |||||
{ | |||||
// Because the uncompressed size of the file is unknown, | |||||
// we are using an arbitrary buffer size. | |||||
byte[] buffer = new byte[4096]; | |||||
int n; | |||||
using(var fs = File.Create(fileName)) | |||||
using (var input = new GZipStream(new MemoryStream(content), | |||||
CompressionMode.Decompress, false)) | |||||
{ | |||||
while ((n = input.Read(buffer, 0, buffer.Length)) > 0) | |||||
{ | |||||
fs.Write(buffer, 0, n); | |||||
} | |||||
} | |||||
} | |||||
public static string NonExclusiveReadAllText(string path) | |||||
{ | |||||
return NonExclusiveReadAllText(path, Encoding.Default); | |||||
} | |||||
public static string NonExclusiveReadAllText(string path, Encoding encoding) | |||||
{ | |||||
try | |||||
{ | |||||
using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) | |||||
using (var sr = new StreamReader(fs, encoding)) | |||||
{ | |||||
return sr.ReadToEnd(); | |||||
} | |||||
} | |||||
catch (Exception ex) | |||||
{ | |||||
logger.Error(ex); | |||||
throw ex; | |||||
} | |||||
} | |||||
} | |||||
} |
@@ -1,11 +1,9 @@ | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.Windows.Forms; | |||||
using NLog; | using NLog; | ||||
using Shadowsocks.Controller.Hotkeys; | using Shadowsocks.Controller.Hotkeys; | ||||
using Shadowsocks.Model; | |||||
using System; | |||||
using System.Windows.Forms; | |||||
namespace Shadowsocks.Controller | |||||
namespace Shadowsocks.WPF.Behaviors | |||||
{ | { | ||||
static class HotkeyReg | static class HotkeyReg | ||||
{ | { |
@@ -1,9 +1,9 @@ | |||||
using System; | |||||
using System; | |||||
using System.IO.Pipes; | using System.IO.Pipes; | ||||
using System.Net; | using System.Net; | ||||
using System.Text; | using System.Text; | ||||
namespace Shadowsocks.Controller | |||||
namespace Shadowsocks.WPF.Behaviors | |||||
{ | { | ||||
class RequestAddUrlEventArgs : EventArgs | class RequestAddUrlEventArgs : EventArgs | ||||
{ | { | ||||
@@ -11,7 +11,7 @@ namespace Shadowsocks.Controller | |||||
public RequestAddUrlEventArgs(string url) | public RequestAddUrlEventArgs(string url) | ||||
{ | { | ||||
this.Url = url; | |||||
Url = url; | |||||
} | } | ||||
} | } | ||||
@@ -1,4 +1,4 @@ | |||||
using Shadowsocks.Common.SystemProxy; | |||||
using Shadowsocks.Net.SystemProxy; | |||||
using System; | using System; | ||||
using System.ComponentModel; | using System.ComponentModel; |
@@ -1,13 +1,11 @@ | |||||
using System; | |||||
using System; | |||||
using System.Collections.Generic; | using System.Collections.Generic; | ||||
using System.Linq; | using System.Linq; | ||||
using System.Net; | |||||
using System.Net.Http; | |||||
using System.Threading.Tasks; | using System.Threading.Tasks; | ||||
using Newtonsoft.Json.Linq; | using Newtonsoft.Json.Linq; | ||||
using Shadowsocks.Model; | |||||
using Shadowsocks.Models; | |||||
namespace Shadowsocks.Controller.Service | |||||
namespace Shadowsocks.WPF.Behaviors | |||||
{ | { | ||||
public class OnlineConfigResolver | public class OnlineConfigResolver | ||||
{ | { |
@@ -1,13 +1,8 @@ | |||||
using Microsoft.Win32; | |||||
using Microsoft.Win32; | |||||
using NLog; | using NLog; | ||||
using Shadowsocks.Util; | |||||
using System; | using System; | ||||
using System.Collections.Generic; | |||||
using System.Linq; | |||||
using System.Text; | |||||
using System.Threading.Tasks; | |||||
namespace Shadowsocks.Controller | |||||
namespace Shadowsocks.WPF.Behaviors | |||||
{ | { | ||||
static class ProtocolHandler | static class ProtocolHandler | ||||
{ | { |
@@ -1,70 +1,69 @@ | |||||
using System; | |||||
using System.Windows.Forms; | |||||
using NLog; | |||||
using Shadowsocks.Model; | |||||
using Shadowsocks.Util.SystemProxy; | |||||
namespace Shadowsocks.Controller | |||||
{ | |||||
public static class SystemProxy | |||||
{ | |||||
private static Logger logger = LogManager.GetCurrentClassLogger(); | |||||
public static void Update(Configuration config, bool forceDisable, PACServer pacSrv, bool noRetry = false) | |||||
{ | |||||
bool global = config.global; | |||||
bool enabled = config.enabled; | |||||
if (forceDisable || !WinINet.operational) | |||||
{ | |||||
enabled = false; | |||||
} | |||||
try | |||||
{ | |||||
if (enabled) | |||||
{ | |||||
if (global) | |||||
{ | |||||
WinINet.ProxyGlobal("localhost:" + config.localPort.ToString(), "<local>"); | |||||
} | |||||
else | |||||
{ | |||||
string pacUrl; | |||||
if (config.useOnlinePac && !string.IsNullOrEmpty(config.pacUrl)) | |||||
{ | |||||
pacUrl = config.pacUrl; | |||||
} | |||||
else | |||||
{ | |||||
pacUrl = pacSrv.PacUrl; | |||||
} | |||||
WinINet.ProxyPAC(pacUrl); | |||||
} | |||||
} | |||||
else | |||||
{ | |||||
WinINet.Restore(); | |||||
} | |||||
} | |||||
catch (ProxyException ex) | |||||
{ | |||||
logger.LogUsefulException(ex); | |||||
if (ex.Type != ProxyExceptionType.Unspecific && !noRetry) | |||||
{ | |||||
var ret = MessageBox.Show(I18N.GetString("Error occured when process proxy setting, do you want reset current setting and retry?"), I18N.GetString("Shadowsocks"), MessageBoxButtons.YesNo, MessageBoxIcon.Warning); | |||||
if (ret == DialogResult.Yes) | |||||
{ | |||||
WinINet.Reset(); | |||||
Update(config, forceDisable, pacSrv, true); | |||||
} | |||||
} | |||||
else | |||||
{ | |||||
MessageBox.Show(I18N.GetString("Unrecoverable proxy setting error occured, see log for detail"), I18N.GetString("Shadowsocks"), MessageBoxButtons.OK, MessageBoxIcon.Error); | |||||
} | |||||
} | |||||
} | |||||
} | |||||
using NLog; | |||||
using Shadowsocks.Net.SystemProxy; | |||||
using Shadowsocks.WPF.Services.SystemProxy; | |||||
using System.Windows; | |||||
namespace Shadowsocks.WPF.Behaviors | |||||
{ | |||||
public static class SystemProxy | |||||
{ | |||||
private static Logger logger = LogManager.GetCurrentClassLogger(); | |||||
public static void Update(Configuration config, bool forceDisable, PACServer pacSrv, bool noRetry = false) | |||||
{ | |||||
bool global = config.global; | |||||
bool enabled = config.enabled; | |||||
if (forceDisable || !WinINet.operational) | |||||
{ | |||||
enabled = false; | |||||
} | |||||
try | |||||
{ | |||||
if (enabled) | |||||
{ | |||||
if (global) | |||||
{ | |||||
WinINet.ProxyGlobal("localhost:" + config.localPort.ToString(), "<local>"); | |||||
} | |||||
else | |||||
{ | |||||
string pacUrl; | |||||
if (config.useOnlinePac && !string.IsNullOrEmpty(config.pacUrl)) | |||||
{ | |||||
pacUrl = config.pacUrl; | |||||
} | |||||
else | |||||
{ | |||||
pacUrl = pacSrv.PacUrl; | |||||
} | |||||
WinINet.ProxyPAC(pacUrl); | |||||
} | |||||
} | |||||
else | |||||
{ | |||||
WinINet.Restore(); | |||||
} | |||||
} | |||||
catch (ProxyException ex) | |||||
{ | |||||
logger.LogUsefulException(ex); | |||||
if (ex.Type != ProxyExceptionType.Unspecific && !noRetry) | |||||
{ | |||||
var ret = MessageBox.Show(I18N.GetString("Error occured when process proxy setting, do you want reset current setting and retry?"), I18N.GetString("Shadowsocks"), MessageBoxButtons.YesNo, MessageBoxIcon.Warning); | |||||
if (ret == DialogResult.Yes) | |||||
{ | |||||
WinINet.Reset(); | |||||
Update(config, forceDisable, pacSrv, true); | |||||
} | |||||
} | |||||
else | |||||
{ | |||||
MessageBox.Show(I18N.GetString("Unrecoverable proxy setting error occured, see log for detail"), I18N.GetString("Shadowsocks"), MessageBoxButtons.OK, MessageBoxIcon.Error); | |||||
} | |||||
} | |||||
} | |||||
} | |||||
} | } |
@@ -0,0 +1,125 @@ | |||||
using Microsoft.Win32; | |||||
using NLog; | |||||
using System; | |||||
using System.Diagnostics; | |||||
using System.Drawing; | |||||
using System.IO; | |||||
using System.Runtime.InteropServices; | |||||
using System.Windows.Forms; | |||||
using ZXing; | |||||
using ZXing.Common; | |||||
using ZXing.QrCode; | |||||
namespace Shadowsocks.WPF.Behaviors | |||||
{ | |||||
public static class Utilities | |||||
{ | |||||
private static Logger logger = LogManager.GetCurrentClassLogger(); | |||||
private static string _tempPath = null; | |||||
// return path to store temporary files | |||||
public static string GetTempPath() | |||||
{ | |||||
if (_tempPath == null) | |||||
{ | |||||
bool isPortableMode = Configuration.Load().portableMode; | |||||
try | |||||
{ | |||||
if (isPortableMode) | |||||
{ | |||||
_tempPath = Directory.CreateDirectory("ss_win_temp").FullName; | |||||
// don't use "/", it will fail when we call explorer /select xxx/ss_win_temp\xxx.log | |||||
} | |||||
else | |||||
{ | |||||
_tempPath = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), @"Shadowsocks\ss_win_temp_" + Program.ExecutablePath.GetHashCode())).FullName; | |||||
} | |||||
} | |||||
catch (Exception e) | |||||
{ | |||||
logger.Error(e); | |||||
throw; | |||||
} | |||||
} | |||||
return _tempPath; | |||||
} | |||||
// return a full path with filename combined which pointed to the temporary directory | |||||
public static string GetTempPath(string filename) => Path.Combine(GetTempPath(), filename); | |||||
public static string ScanQRCodeFromScreen() | |||||
{ | |||||
foreach (Screen screen in Screen.AllScreens) | |||||
{ | |||||
using (Bitmap fullImage = new Bitmap(screen.Bounds.Width, | |||||
screen.Bounds.Height)) | |||||
{ | |||||
using (Graphics g = Graphics.FromImage(fullImage)) | |||||
{ | |||||
g.CopyFromScreen(screen.Bounds.X, | |||||
screen.Bounds.Y, | |||||
0, 0, | |||||
fullImage.Size, | |||||
CopyPixelOperation.SourceCopy); | |||||
} | |||||
int maxTry = 10; | |||||
for (int i = 0; i < maxTry; i++) | |||||
{ | |||||
int marginLeft = (int)((double)fullImage.Width * i / 2.5 / maxTry); | |||||
int marginTop = (int)((double)fullImage.Height * i / 2.5 / maxTry); | |||||
Rectangle cropRect = new Rectangle(marginLeft, marginTop, fullImage.Width - marginLeft * 2, fullImage.Height - marginTop * 2); | |||||
Bitmap target = new Bitmap(screen.Bounds.Width, screen.Bounds.Height); | |||||
double imageScale = (double)screen.Bounds.Width / (double)cropRect.Width; | |||||
using (Graphics g = Graphics.FromImage(target)) | |||||
{ | |||||
g.DrawImage(fullImage, new Rectangle(0, 0, target.Width, target.Height), | |||||
cropRect, | |||||
GraphicsUnit.Pixel); | |||||
} | |||||
var source = new BitmapLuminanceSource(target); | |||||
var bitmap = new BinaryBitmap(new HybridBinarizer(source)); | |||||
QRCodeReader reader = new QRCodeReader(); | |||||
var result = reader.decode(bitmap); | |||||
if (result != null) | |||||
return result.Text; | |||||
} | |||||
} | |||||
} | |||||
return null; | |||||
} | |||||
public static void OpenInBrowser(string url) | |||||
{ | |||||
try | |||||
{ | |||||
Process.Start(url); | |||||
} | |||||
catch | |||||
{ | |||||
// hack because of this: https://github.com/dotnet/corefx/issues/10361 | |||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) | |||||
{ | |||||
Process.Start(new ProcessStartInfo(url) | |||||
{ | |||||
UseShellExecute = true, | |||||
Verb = "open" | |||||
}); | |||||
} | |||||
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) | |||||
{ | |||||
Process.Start("xdg-open", url); | |||||
} | |||||
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) | |||||
{ | |||||
Process.Start("open", url); | |||||
} | |||||
else | |||||
{ | |||||
throw; | |||||
} | |||||
} | |||||
} | |||||
} | |||||
} |
@@ -1,12 +1,9 @@ | |||||
using Shadowsocks.Common.Model; | |||||
using System.Reflection; | using System.Reflection; | ||||
using WPFLocalizeExtension.Extensions; | using WPFLocalizeExtension.Extensions; | ||||
namespace Shadowsocks.WPF.Localization | namespace Shadowsocks.WPF.Localization | ||||
{ | { | ||||
public class LocalizationProvider : ILocalizationProvider | |||||
public class LocalizationProvider | |||||
{ | { | ||||
private static readonly string CallingAssemblyName = Assembly.GetCallingAssembly().GetName().Name; | private static readonly string CallingAssemblyName = Assembly.GetCallingAssembly().GetName().Name; | ||||
@@ -1,6 +1,6 @@ | |||||
using System; | |||||
using System; | |||||
namespace Shadowsocks.Model | |||||
namespace Shadowsocks.WPF.Models | |||||
{ | { | ||||
[Serializable] | [Serializable] | ||||
public class ForwardProxyConfig | public class ForwardProxyConfig |
@@ -1,33 +1,33 @@ | |||||
using System; | |||||
namespace Shadowsocks.Model | |||||
{ | |||||
/* | |||||
* Format: | |||||
* <modifiers-combination>+<key> | |||||
* | |||||
*/ | |||||
[Serializable] | |||||
public class HotkeyConfig | |||||
{ | |||||
public string SwitchSystemProxy; | |||||
public string SwitchSystemProxyMode; | |||||
public string SwitchAllowLan; | |||||
public string ShowLogs; | |||||
public string ServerMoveUp; | |||||
public string ServerMoveDown; | |||||
public bool RegHotkeysAtStartup; | |||||
public HotkeyConfig() | |||||
{ | |||||
SwitchSystemProxy = ""; | |||||
SwitchSystemProxyMode = ""; | |||||
SwitchAllowLan = ""; | |||||
ShowLogs = ""; | |||||
ServerMoveUp = ""; | |||||
ServerMoveDown = ""; | |||||
RegHotkeysAtStartup = false; | |||||
} | |||||
} | |||||
using System; | |||||
namespace Shadowsocks.WPF.Models | |||||
{ | |||||
/* | |||||
* Format: | |||||
* <modifiers-combination>+<key> | |||||
* | |||||
*/ | |||||
[Serializable] | |||||
public class HotkeyConfig | |||||
{ | |||||
public string SwitchSystemProxy; | |||||
public string SwitchSystemProxyMode; | |||||
public string SwitchAllowLan; | |||||
public string ShowLogs; | |||||
public string ServerMoveUp; | |||||
public string ServerMoveDown; | |||||
public bool RegHotkeysAtStartup; | |||||
public HotkeyConfig() | |||||
{ | |||||
SwitchSystemProxy = ""; | |||||
SwitchSystemProxyMode = ""; | |||||
SwitchAllowLan = ""; | |||||
ShowLogs = ""; | |||||
ServerMoveUp = ""; | |||||
ServerMoveDown = ""; | |||||
RegHotkeysAtStartup = false; | |||||
} | |||||
} | |||||
} | } |
@@ -1,137 +1,137 @@ | |||||
using NLog; | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.IO; | |||||
using System.Linq; | |||||
using System.Text; | |||||
using System.Threading.Tasks; | |||||
using System.Xml; | |||||
namespace Shadowsocks.Model | |||||
{ | |||||
public class NLogConfig | |||||
{ | |||||
public enum LogLevel | |||||
{ | |||||
Fatal, | |||||
Error, | |||||
Warn, | |||||
Info, | |||||
Debug, | |||||
Trace, | |||||
} | |||||
private static string _NLOG_CONFIG_FILE_NAME=string.Empty; | |||||
public static string NLOG_CONFIG_FILE_NAME | |||||
{ | |||||
get | |||||
{ | |||||
if (string.IsNullOrEmpty(_NLOG_CONFIG_FILE_NAME)) | |||||
{ | |||||
_NLOG_CONFIG_FILE_NAME = Path.Combine(Environment.CurrentDirectory, "NLog.config"); | |||||
} | |||||
return _NLOG_CONFIG_FILE_NAME; | |||||
} | |||||
} | |||||
const string TARGET_MIN_LEVEL_ATTRIBUTE = "minlevel"; | |||||
const string LOGGER_FILE_NAME_ATTRIBUTE = "fileName"; | |||||
XmlDocument doc = new XmlDocument(); | |||||
XmlElement logFileNameElement; | |||||
XmlElement logLevelElement; | |||||
/// <summary> | |||||
/// Load the NLog config xml file content | |||||
/// </summary> | |||||
public static NLogConfig LoadXML() | |||||
{ | |||||
NLogConfig config = new NLogConfig(); | |||||
config.doc.Load(NLOG_CONFIG_FILE_NAME); | |||||
config.logLevelElement = (XmlElement)SelectSingleNode(config.doc, "//nlog:logger[@name='*']"); | |||||
config.logFileNameElement = (XmlElement)SelectSingleNode(config.doc, "//nlog:target[@name='file']"); | |||||
return config; | |||||
} | |||||
/// <summary> | |||||
/// Save the content to NLog config xml file | |||||
/// </summary> | |||||
public static void SaveXML(NLogConfig nLogConfig) | |||||
{ | |||||
nLogConfig.doc.Save(NLOG_CONFIG_FILE_NAME); | |||||
} | |||||
/// <summary> | |||||
/// Get the current minLogLevel from xml file | |||||
/// </summary> | |||||
/// <returns></returns> | |||||
public LogLevel GetLogLevel() | |||||
{ | |||||
LogLevel level = LogLevel.Warn; | |||||
string levelStr = logLevelElement.GetAttribute(TARGET_MIN_LEVEL_ATTRIBUTE); | |||||
Enum.TryParse(levelStr, out level); | |||||
return level; | |||||
} | |||||
/// <summary> | |||||
/// Get the target fileName from xml file | |||||
/// </summary> | |||||
/// <returns></returns> | |||||
public string GetLogFileName() | |||||
{ | |||||
return logFileNameElement.GetAttribute(LOGGER_FILE_NAME_ATTRIBUTE); | |||||
} | |||||
/// <summary> | |||||
/// Set the minLogLevel to xml file | |||||
/// </summary> | |||||
/// <param name="logLevel"></param> | |||||
public void SetLogLevel(LogLevel logLevel) | |||||
{ | |||||
logLevelElement.SetAttribute(TARGET_MIN_LEVEL_ATTRIBUTE, logLevel.ToString("G")); | |||||
} | |||||
/// <summary> | |||||
/// Set the target fileName to xml file | |||||
/// </summary> | |||||
/// <param name="fileName"></param> | |||||
public void SetLogFileName(string fileName) | |||||
{ | |||||
logFileNameElement.SetAttribute(LOGGER_FILE_NAME_ATTRIBUTE, fileName); | |||||
} | |||||
/// <summary> | |||||
/// Select a single XML node/elemant | |||||
/// </summary> | |||||
/// <param name="doc"></param> | |||||
/// <param name="xpath"></param> | |||||
/// <returns></returns> | |||||
private static XmlNode SelectSingleNode(XmlDocument doc, string xpath) | |||||
{ | |||||
XmlNamespaceManager manager = new XmlNamespaceManager(doc.NameTable); | |||||
manager.AddNamespace("nlog", "http://www.nlog-project.org/schemas/NLog.xsd"); | |||||
//return doc.SelectSingleNode("//nlog:logger[(@shadowsocks='managed') and (@name='*')]", manager); | |||||
return doc.SelectSingleNode(xpath, manager); | |||||
} | |||||
/// <summary> | |||||
/// Extract the pre-defined NLog configuration file is does not exist. Then reload the Nlog configuration. | |||||
/// </summary> | |||||
public static void TouchAndApplyNLogConfig() | |||||
{ | |||||
if (!File.Exists(NLOG_CONFIG_FILE_NAME)) | |||||
{ | |||||
File.WriteAllText(NLOG_CONFIG_FILE_NAME, Properties.Resources.NLog_config); | |||||
LogManager.LoadConfiguration(NLOG_CONFIG_FILE_NAME); | |||||
} | |||||
} | |||||
/// <summary> | |||||
/// NLog reload the config file and apply to current LogManager | |||||
/// </summary> | |||||
public static void LoadConfiguration() | |||||
{ | |||||
LogManager.LoadConfiguration(NLOG_CONFIG_FILE_NAME); | |||||
} | |||||
} | |||||
} | |||||
using NLog; | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.IO; | |||||
using System.Linq; | |||||
using System.Text; | |||||
using System.Threading.Tasks; | |||||
using System.Xml; | |||||
namespace Shadowsocks.WPF.Models | |||||
{ | |||||
public class NLogConfig | |||||
{ | |||||
public enum LogLevel | |||||
{ | |||||
Fatal, | |||||
Error, | |||||
Warn, | |||||
Info, | |||||
Debug, | |||||
Trace, | |||||
} | |||||
private static string _NLOG_CONFIG_FILE_NAME=string.Empty; | |||||
public static string NLOG_CONFIG_FILE_NAME | |||||
{ | |||||
get | |||||
{ | |||||
if (string.IsNullOrEmpty(_NLOG_CONFIG_FILE_NAME)) | |||||
{ | |||||
_NLOG_CONFIG_FILE_NAME = Path.Combine(Environment.CurrentDirectory, "NLog.config"); | |||||
} | |||||
return _NLOG_CONFIG_FILE_NAME; | |||||
} | |||||
} | |||||
const string TARGET_MIN_LEVEL_ATTRIBUTE = "minlevel"; | |||||
const string LOGGER_FILE_NAME_ATTRIBUTE = "fileName"; | |||||
XmlDocument doc = new XmlDocument(); | |||||
XmlElement logFileNameElement; | |||||
XmlElement logLevelElement; | |||||
/// <summary> | |||||
/// Load the NLog config xml file content | |||||
/// </summary> | |||||
public static NLogConfig LoadXML() | |||||
{ | |||||
NLogConfig config = new NLogConfig(); | |||||
config.doc.Load(NLOG_CONFIG_FILE_NAME); | |||||
config.logLevelElement = (XmlElement)SelectSingleNode(config.doc, "//nlog:logger[@name='*']"); | |||||
config.logFileNameElement = (XmlElement)SelectSingleNode(config.doc, "//nlog:target[@name='file']"); | |||||
return config; | |||||
} | |||||
/// <summary> | |||||
/// Save the content to NLog config xml file | |||||
/// </summary> | |||||
public static void SaveXML(NLogConfig nLogConfig) | |||||
{ | |||||
nLogConfig.doc.Save(NLOG_CONFIG_FILE_NAME); | |||||
} | |||||
/// <summary> | |||||
/// Get the current minLogLevel from xml file | |||||
/// </summary> | |||||
/// <returns></returns> | |||||
public LogLevel GetLogLevel() | |||||
{ | |||||
LogLevel level = LogLevel.Warn; | |||||
string levelStr = logLevelElement.GetAttribute(TARGET_MIN_LEVEL_ATTRIBUTE); | |||||
Enum.TryParse(levelStr, out level); | |||||
return level; | |||||
} | |||||
/// <summary> | |||||
/// Get the target fileName from xml file | |||||
/// </summary> | |||||
/// <returns></returns> | |||||
public string GetLogFileName() | |||||
{ | |||||
return logFileNameElement.GetAttribute(LOGGER_FILE_NAME_ATTRIBUTE); | |||||
} | |||||
/// <summary> | |||||
/// Set the minLogLevel to xml file | |||||
/// </summary> | |||||
/// <param name="logLevel"></param> | |||||
public void SetLogLevel(LogLevel logLevel) | |||||
{ | |||||
logLevelElement.SetAttribute(TARGET_MIN_LEVEL_ATTRIBUTE, logLevel.ToString("G")); | |||||
} | |||||
/// <summary> | |||||
/// Set the target fileName to xml file | |||||
/// </summary> | |||||
/// <param name="fileName"></param> | |||||
public void SetLogFileName(string fileName) | |||||
{ | |||||
logFileNameElement.SetAttribute(LOGGER_FILE_NAME_ATTRIBUTE, fileName); | |||||
} | |||||
/// <summary> | |||||
/// Select a single XML node/elemant | |||||
/// </summary> | |||||
/// <param name="doc"></param> | |||||
/// <param name="xpath"></param> | |||||
/// <returns></returns> | |||||
private static XmlNode SelectSingleNode(XmlDocument doc, string xpath) | |||||
{ | |||||
XmlNamespaceManager manager = new XmlNamespaceManager(doc.NameTable); | |||||
manager.AddNamespace("nlog", "http://www.nlog-project.org/schemas/NLog.xsd"); | |||||
//return doc.SelectSingleNode("//nlog:logger[(@shadowsocks='managed') and (@name='*')]", manager); | |||||
return doc.SelectSingleNode(xpath, manager); | |||||
} | |||||
/// <summary> | |||||
/// Extract the pre-defined NLog configuration file is does not exist. Then reload the Nlog configuration. | |||||
/// </summary> | |||||
public static void TouchAndApplyNLogConfig() | |||||
{ | |||||
if (!File.Exists(NLOG_CONFIG_FILE_NAME)) | |||||
{ | |||||
File.WriteAllText(NLOG_CONFIG_FILE_NAME, Properties.Resources.NLog_config); | |||||
LogManager.LoadConfiguration(NLOG_CONFIG_FILE_NAME); | |||||
} | |||||
} | |||||
/// <summary> | |||||
/// NLog reload the config file and apply to current LogManager | |||||
/// </summary> | |||||
public static void LoadConfiguration() | |||||
{ | |||||
LogManager.LoadConfiguration(NLOG_CONFIG_FILE_NAME); | |||||
} | |||||
} | |||||
} |
@@ -1,252 +1,252 @@ | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.Collections.Specialized; | |||||
using System.Text; | |||||
using System.Web; | |||||
using Shadowsocks.Controller; | |||||
using System.Text.RegularExpressions; | |||||
using System.Linq; | |||||
using Newtonsoft.Json; | |||||
using System.ComponentModel; | |||||
namespace Shadowsocks.Model | |||||
{ | |||||
[Serializable] | |||||
public class Server | |||||
{ | |||||
public const string DefaultMethod = "chacha20-ietf-poly1305"; | |||||
public const int DefaultPort = 8388; | |||||
#region ParseLegacyURL | |||||
private static readonly Regex UrlFinder = new Regex(@"ss://(?<base64>[A-Za-z0-9+-/=_]+)(?:#(?<tag>\S+))?", RegexOptions.IgnoreCase); | |||||
private static readonly Regex DetailsParser = new Regex(@"^((?<method>.+?):(?<password>.*)@(?<hostname>.+?):(?<port>\d+?))$", RegexOptions.IgnoreCase); | |||||
#endregion ParseLegacyURL | |||||
private const int DefaultServerTimeoutSec = 5; | |||||
public const int MaxServerTimeoutSec = 20; | |||||
public string server; | |||||
public int server_port; | |||||
public string password; | |||||
public string method; | |||||
// optional fields | |||||
[DefaultValue("")] | |||||
[JsonProperty(NullValueHandling = NullValueHandling.Ignore, DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)] | |||||
public string plugin; | |||||
[DefaultValue("")] | |||||
[JsonProperty(NullValueHandling = NullValueHandling.Ignore, DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)] | |||||
public string plugin_opts; | |||||
[DefaultValue("")] | |||||
[JsonProperty(NullValueHandling = NullValueHandling.Ignore, DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)] | |||||
public string plugin_args; | |||||
[DefaultValue("")] | |||||
[JsonProperty(NullValueHandling = NullValueHandling.Ignore, DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)] | |||||
public string remarks; | |||||
[DefaultValue("")] | |||||
[JsonProperty(NullValueHandling = NullValueHandling.Ignore, DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)] | |||||
public string group; | |||||
public int timeout; | |||||
public override int GetHashCode() | |||||
{ | |||||
return server.GetHashCode() ^ server_port; | |||||
} | |||||
public override bool Equals(object obj) => obj is Server o2 && server == o2.server && server_port == o2.server_port; | |||||
public override string ToString() | |||||
{ | |||||
if (string.IsNullOrEmpty(server)) | |||||
{ | |||||
return I18N.GetString("New server"); | |||||
} | |||||
string serverStr = $"{FormalHostName}:{server_port}"; | |||||
return string.IsNullOrEmpty(remarks) | |||||
? serverStr | |||||
: $"{remarks} ({serverStr})"; | |||||
} | |||||
public string GetURL(bool legacyUrl = false) | |||||
{ | |||||
if (legacyUrl && string.IsNullOrWhiteSpace(plugin)) | |||||
{ | |||||
// For backwards compatiblity, if no plugin, use old url format | |||||
string p = $"{method}:{password}@{server}:{server_port}"; | |||||
string base64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(p)); | |||||
return string.IsNullOrEmpty(remarks) | |||||
? $"ss://{base64}" | |||||
: $"ss://{base64}#{HttpUtility.UrlEncode(remarks, Encoding.UTF8)}"; | |||||
} | |||||
UriBuilder u = new UriBuilder("ss", null); | |||||
string b64 = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{method}:{password}")); | |||||
u.UserName = b64.Replace('+', '-').Replace('/', '_').TrimEnd('='); | |||||
u.Host = server; | |||||
u.Port = server_port; | |||||
u.Fragment = HttpUtility.UrlEncode(remarks, Encoding.UTF8); | |||||
if (!string.IsNullOrWhiteSpace(plugin)) | |||||
{ | |||||
NameValueCollection param = HttpUtility.ParseQueryString(""); | |||||
string pluginPart = plugin; | |||||
if (!string.IsNullOrWhiteSpace(plugin_opts)) | |||||
{ | |||||
pluginPart += ";" + plugin_opts; | |||||
} | |||||
param["plugin"] = pluginPart; | |||||
u.Query = param.ToString(); | |||||
} | |||||
return u.ToString(); | |||||
} | |||||
[JsonIgnore] | |||||
public string FormalHostName | |||||
{ | |||||
get | |||||
{ | |||||
// CheckHostName() won't do a real DNS lookup | |||||
return (Uri.CheckHostName(server)) switch | |||||
{ | |||||
// Add square bracket when IPv6 (RFC3986) | |||||
UriHostNameType.IPv6 => $"[{server}]", | |||||
// IPv4 or domain name | |||||
_ => server, | |||||
}; | |||||
} | |||||
} | |||||
public Server() | |||||
{ | |||||
server = ""; | |||||
server_port = DefaultPort; | |||||
method = DefaultMethod; | |||||
plugin = ""; | |||||
plugin_opts = ""; | |||||
plugin_args = ""; | |||||
password = ""; | |||||
remarks = ""; | |||||
timeout = DefaultServerTimeoutSec; | |||||
} | |||||
private static Server ParseLegacyURL(string ssURL) | |||||
{ | |||||
var match = UrlFinder.Match(ssURL); | |||||
if (!match.Success) | |||||
return null; | |||||
Server server = new Server(); | |||||
var base64 = match.Groups["base64"].Value.TrimEnd('/'); | |||||
var tag = match.Groups["tag"].Value; | |||||
if (!string.IsNullOrEmpty(tag)) | |||||
{ | |||||
server.remarks = HttpUtility.UrlDecode(tag, Encoding.UTF8); | |||||
} | |||||
Match details; | |||||
try | |||||
{ | |||||
details = DetailsParser.Match(Encoding.UTF8.GetString(Convert.FromBase64String( | |||||
base64.PadRight(base64.Length + (4 - base64.Length % 4) % 4, '=')))); | |||||
} | |||||
catch (FormatException) | |||||
{ | |||||
return null; | |||||
} | |||||
if (!details.Success) | |||||
return null; | |||||
server.method = details.Groups["method"].Value; | |||||
server.password = details.Groups["password"].Value; | |||||
server.server = details.Groups["hostname"].Value; | |||||
server.server_port = int.Parse(details.Groups["port"].Value); | |||||
return server; | |||||
} | |||||
public static Server ParseURL(string serverUrl) | |||||
{ | |||||
string _serverUrl = serverUrl.Trim(); | |||||
if (!_serverUrl.StartsWith("ss://", StringComparison.InvariantCultureIgnoreCase)) | |||||
{ | |||||
return null; | |||||
} | |||||
Server legacyServer = ParseLegacyURL(serverUrl); | |||||
if (legacyServer != null) //legacy | |||||
{ | |||||
return legacyServer; | |||||
} | |||||
else //SIP002 | |||||
{ | |||||
Uri parsedUrl; | |||||
try | |||||
{ | |||||
parsedUrl = new Uri(serverUrl); | |||||
} | |||||
catch (UriFormatException) | |||||
{ | |||||
return null; | |||||
} | |||||
Server server = new Server | |||||
{ | |||||
remarks = HttpUtility.UrlDecode(parsedUrl.GetComponents( | |||||
UriComponents.Fragment, UriFormat.Unescaped), Encoding.UTF8), | |||||
server = parsedUrl.IdnHost, | |||||
server_port = parsedUrl.Port, | |||||
}; | |||||
// parse base64 UserInfo | |||||
string rawUserInfo = parsedUrl.GetComponents(UriComponents.UserInfo, UriFormat.Unescaped); | |||||
string base64 = rawUserInfo.Replace('-', '+').Replace('_', '/'); // Web-safe base64 to normal base64 | |||||
string userInfo; | |||||
try | |||||
{ | |||||
userInfo = Encoding.UTF8.GetString(Convert.FromBase64String( | |||||
base64.PadRight(base64.Length + (4 - base64.Length % 4) % 4, '='))); | |||||
} | |||||
catch (FormatException) | |||||
{ | |||||
return null; | |||||
} | |||||
string[] userInfoParts = userInfo.Split(new char[] { ':' }, 2); | |||||
if (userInfoParts.Length != 2) | |||||
{ | |||||
return null; | |||||
} | |||||
server.method = userInfoParts[0]; | |||||
server.password = userInfoParts[1]; | |||||
NameValueCollection queryParameters = HttpUtility.ParseQueryString(parsedUrl.Query); | |||||
string[] pluginParts = (queryParameters["plugin"] ?? "").Split(new[] { ';' }, 2); | |||||
if (pluginParts.Length > 0) | |||||
{ | |||||
server.plugin = pluginParts[0] ?? ""; | |||||
} | |||||
if (pluginParts.Length > 1) | |||||
{ | |||||
server.plugin_opts = pluginParts[1] ?? ""; | |||||
} | |||||
return server; | |||||
} | |||||
} | |||||
public static List<Server> GetServers(string ssURL) | |||||
{ | |||||
return ssURL | |||||
.Split('\r', '\n', ' ') | |||||
.Select(u => ParseURL(u)) | |||||
.Where(s => s != null) | |||||
.ToList(); | |||||
} | |||||
public string Identifier() | |||||
{ | |||||
return server + ':' + server_port; | |||||
} | |||||
} | |||||
} | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.Collections.Specialized; | |||||
using System.Text; | |||||
using System.Web; | |||||
using Shadowsocks.Controller; | |||||
using System.Text.RegularExpressions; | |||||
using System.Linq; | |||||
using Newtonsoft.Json; | |||||
using System.ComponentModel; | |||||
namespace Shadowsocks.WPF.Models | |||||
{ | |||||
[Serializable] | |||||
public class Server | |||||
{ | |||||
public const string DefaultMethod = "chacha20-ietf-poly1305"; | |||||
public const int DefaultPort = 8388; | |||||
#region ParseLegacyURL | |||||
private static readonly Regex UrlFinder = new Regex(@"ss://(?<base64>[A-Za-z0-9+-/=_]+)(?:#(?<tag>\S+))?", RegexOptions.IgnoreCase); | |||||
private static readonly Regex DetailsParser = new Regex(@"^((?<method>.+?):(?<password>.*)@(?<hostname>.+?):(?<port>\d+?))$", RegexOptions.IgnoreCase); | |||||
#endregion ParseLegacyURL | |||||
private const int DefaultServerTimeoutSec = 5; | |||||
public const int MaxServerTimeoutSec = 20; | |||||
public string server; | |||||
public int server_port; | |||||
public string password; | |||||
public string method; | |||||
// optional fields | |||||
[DefaultValue("")] | |||||
[JsonProperty(NullValueHandling = NullValueHandling.Ignore, DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)] | |||||
public string plugin; | |||||
[DefaultValue("")] | |||||
[JsonProperty(NullValueHandling = NullValueHandling.Ignore, DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)] | |||||
public string plugin_opts; | |||||
[DefaultValue("")] | |||||
[JsonProperty(NullValueHandling = NullValueHandling.Ignore, DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)] | |||||
public string plugin_args; | |||||
[DefaultValue("")] | |||||
[JsonProperty(NullValueHandling = NullValueHandling.Ignore, DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)] | |||||
public string remarks; | |||||
[DefaultValue("")] | |||||
[JsonProperty(NullValueHandling = NullValueHandling.Ignore, DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)] | |||||
public string group; | |||||
public int timeout; | |||||
public override int GetHashCode() | |||||
{ | |||||
return server.GetHashCode() ^ server_port; | |||||
} | |||||
public override bool Equals(object obj) => obj is Server o2 && server == o2.server && server_port == o2.server_port; | |||||
public override string ToString() | |||||
{ | |||||
if (string.IsNullOrEmpty(server)) | |||||
{ | |||||
return I18N.GetString("New server"); | |||||
} | |||||
string serverStr = $"{FormalHostName}:{server_port}"; | |||||
return string.IsNullOrEmpty(remarks) | |||||
? serverStr | |||||
: $"{remarks} ({serverStr})"; | |||||
} | |||||
public string GetURL(bool legacyUrl = false) | |||||
{ | |||||
if (legacyUrl && string.IsNullOrWhiteSpace(plugin)) | |||||
{ | |||||
// For backwards compatiblity, if no plugin, use old url format | |||||
string p = $"{method}:{password}@{server}:{server_port}"; | |||||
string base64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(p)); | |||||
return string.IsNullOrEmpty(remarks) | |||||
? $"ss://{base64}" | |||||
: $"ss://{base64}#{HttpUtility.UrlEncode(remarks, Encoding.UTF8)}"; | |||||
} | |||||
UriBuilder u = new UriBuilder("ss", null); | |||||
string b64 = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{method}:{password}")); | |||||
u.UserName = b64.Replace('+', '-').Replace('/', '_').TrimEnd('='); | |||||
u.Host = server; | |||||
u.Port = server_port; | |||||
u.Fragment = HttpUtility.UrlEncode(remarks, Encoding.UTF8); | |||||
if (!string.IsNullOrWhiteSpace(plugin)) | |||||
{ | |||||
NameValueCollection param = HttpUtility.ParseQueryString(""); | |||||
string pluginPart = plugin; | |||||
if (!string.IsNullOrWhiteSpace(plugin_opts)) | |||||
{ | |||||
pluginPart += ";" + plugin_opts; | |||||
} | |||||
param["plugin"] = pluginPart; | |||||
u.Query = param.ToString(); | |||||
} | |||||
return u.ToString(); | |||||
} | |||||
[JsonIgnore] | |||||
public string FormalHostName | |||||
{ | |||||
get | |||||
{ | |||||
// CheckHostName() won't do a real DNS lookup | |||||
return (Uri.CheckHostName(server)) switch | |||||
{ | |||||
// Add square bracket when IPv6 (RFC3986) | |||||
UriHostNameType.IPv6 => $"[{server}]", | |||||
// IPv4 or domain name | |||||
_ => server, | |||||
}; | |||||
} | |||||
} | |||||
public Server() | |||||
{ | |||||
server = ""; | |||||
server_port = DefaultPort; | |||||
method = DefaultMethod; | |||||
plugin = ""; | |||||
plugin_opts = ""; | |||||
plugin_args = ""; | |||||
password = ""; | |||||
remarks = ""; | |||||
timeout = DefaultServerTimeoutSec; | |||||
} | |||||
private static Server ParseLegacyURL(string ssURL) | |||||
{ | |||||
var match = UrlFinder.Match(ssURL); | |||||
if (!match.Success) | |||||
return null; | |||||
Server server = new Server(); | |||||
var base64 = match.Groups["base64"].Value.TrimEnd('/'); | |||||
var tag = match.Groups["tag"].Value; | |||||
if (!string.IsNullOrEmpty(tag)) | |||||
{ | |||||
server.remarks = HttpUtility.UrlDecode(tag, Encoding.UTF8); | |||||
} | |||||
Match details; | |||||
try | |||||
{ | |||||
details = DetailsParser.Match(Encoding.UTF8.GetString(Convert.FromBase64String( | |||||
base64.PadRight(base64.Length + (4 - base64.Length % 4) % 4, '=')))); | |||||
} | |||||
catch (FormatException) | |||||
{ | |||||
return null; | |||||
} | |||||
if (!details.Success) | |||||
return null; | |||||
server.method = details.Groups["method"].Value; | |||||
server.password = details.Groups["password"].Value; | |||||
server.server = details.Groups["hostname"].Value; | |||||
server.server_port = int.Parse(details.Groups["port"].Value); | |||||
return server; | |||||
} | |||||
public static Server ParseURL(string serverUrl) | |||||
{ | |||||
string _serverUrl = serverUrl.Trim(); | |||||
if (!_serverUrl.StartsWith("ss://", StringComparison.InvariantCultureIgnoreCase)) | |||||
{ | |||||
return null; | |||||
} | |||||
Server legacyServer = ParseLegacyURL(serverUrl); | |||||
if (legacyServer != null) //legacy | |||||
{ | |||||
return legacyServer; | |||||
} | |||||
else //SIP002 | |||||
{ | |||||
Uri parsedUrl; | |||||
try | |||||
{ | |||||
parsedUrl = new Uri(serverUrl); | |||||
} | |||||
catch (UriFormatException) | |||||
{ | |||||
return null; | |||||
} | |||||
Server server = new Server | |||||
{ | |||||
remarks = HttpUtility.UrlDecode(parsedUrl.GetComponents( | |||||
UriComponents.Fragment, UriFormat.Unescaped), Encoding.UTF8), | |||||
server = parsedUrl.IdnHost, | |||||
server_port = parsedUrl.Port, | |||||
}; | |||||
// parse base64 UserInfo | |||||
string rawUserInfo = parsedUrl.GetComponents(UriComponents.UserInfo, UriFormat.Unescaped); | |||||
string base64 = rawUserInfo.Replace('-', '+').Replace('_', '/'); // Web-safe base64 to normal base64 | |||||
string userInfo; | |||||
try | |||||
{ | |||||
userInfo = Encoding.UTF8.GetString(Convert.FromBase64String( | |||||
base64.PadRight(base64.Length + (4 - base64.Length % 4) % 4, '='))); | |||||
} | |||||
catch (FormatException) | |||||
{ | |||||
return null; | |||||
} | |||||
string[] userInfoParts = userInfo.Split(new char[] { ':' }, 2); | |||||
if (userInfoParts.Length != 2) | |||||
{ | |||||
return null; | |||||
} | |||||
server.method = userInfoParts[0]; | |||||
server.password = userInfoParts[1]; | |||||
NameValueCollection queryParameters = HttpUtility.ParseQueryString(parsedUrl.Query); | |||||
string[] pluginParts = (queryParameters["plugin"] ?? "").Split(new[] { ';' }, 2); | |||||
if (pluginParts.Length > 0) | |||||
{ | |||||
server.plugin = pluginParts[0] ?? ""; | |||||
} | |||||
if (pluginParts.Length > 1) | |||||
{ | |||||
server.plugin_opts = pluginParts[1] ?? ""; | |||||
} | |||||
return server; | |||||
} | |||||
} | |||||
public static List<Server> GetServers(string ssURL) | |||||
{ | |||||
return ssURL | |||||
.Split('\r', '\n', ' ') | |||||
.Select(u => ParseURL(u)) | |||||
.Where(s => s != null) | |||||
.ToList(); | |||||
} | |||||
public string Identifier() | |||||
{ | |||||
return server + ':' + server_port; | |||||
} | |||||
} | |||||
} |
@@ -0,0 +1,16 @@ | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.Text; | |||||
namespace Shadowsocks.WPF.Models | |||||
{ | |||||
public class Settings | |||||
{ | |||||
public Settings() | |||||
{ | |||||
} | |||||
} | |||||
} |
@@ -1,277 +1,276 @@ | |||||
using System; | |||||
using System.Net; | |||||
using System.Net.Sockets; | |||||
using NLog; | |||||
using Shadowsocks.Util.Sockets; | |||||
namespace Shadowsocks.Controller | |||||
{ | |||||
class PortForwarder : StreamService | |||||
{ | |||||
private readonly int _targetPort; | |||||
public PortForwarder(int targetPort) | |||||
{ | |||||
_targetPort = targetPort; | |||||
} | |||||
public override bool Handle(CachedNetworkStream stream, object state) | |||||
{ | |||||
byte[] fp = new byte[256]; | |||||
int len = stream.ReadFirstBlock(fp); | |||||
if (stream.Socket.ProtocolType != ProtocolType.Tcp) | |||||
{ | |||||
return false; | |||||
} | |||||
new Handler().Start(fp, len, stream.Socket, _targetPort); | |||||
return true; | |||||
} | |||||
[Obsolete] | |||||
public override bool Handle(byte[] firstPacket, int length, Socket socket, object state) | |||||
{ | |||||
if (socket.ProtocolType != ProtocolType.Tcp) | |||||
{ | |||||
return false; | |||||
} | |||||
new Handler().Start(firstPacket, length, socket, _targetPort); | |||||
return true; | |||||
} | |||||
private class Handler | |||||
{ | |||||
private static Logger logger = LogManager.GetCurrentClassLogger(); | |||||
private byte[] _firstPacket; | |||||
private int _firstPacketLength; | |||||
private Socket _local; | |||||
private Socket _remote; | |||||
private bool _closed = false; | |||||
private bool _localShutdown = false; | |||||
private bool _remoteShutdown = false; | |||||
private const int RecvSize = 2048; | |||||
// remote receive buffer | |||||
private byte[] remoteRecvBuffer = new byte[RecvSize]; | |||||
// connection receive buffer | |||||
private byte[] connetionRecvBuffer = new byte[RecvSize]; | |||||
// instance-based lock | |||||
private readonly object _Lock = new object(); | |||||
public void Start(byte[] firstPacket, int length, Socket socket, int targetPort) | |||||
{ | |||||
_firstPacket = firstPacket; | |||||
_firstPacketLength = length; | |||||
_local = socket; | |||||
try | |||||
{ | |||||
// Local Port Forward use IP as is | |||||
EndPoint remoteEP = SocketUtil.GetEndPoint(_local.AddressFamily == AddressFamily.InterNetworkV6 ? "[::1]" : "127.0.0.1", targetPort); | |||||
// Connect to the remote endpoint. | |||||
_remote = new Socket(SocketType.Stream, ProtocolType.Tcp); | |||||
_remote.BeginConnect(remoteEP, ConnectCallback, null); | |||||
} | |||||
catch (Exception e) | |||||
{ | |||||
logger.LogUsefulException(e); | |||||
Close(); | |||||
} | |||||
} | |||||
private void ConnectCallback(IAsyncResult ar) | |||||
{ | |||||
if (_closed) | |||||
{ | |||||
return; | |||||
} | |||||
try | |||||
{ | |||||
_remote.EndConnect(ar); | |||||
_remote.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.NoDelay, true); | |||||
HandshakeReceive(); | |||||
} | |||||
catch (Exception e) | |||||
{ | |||||
logger.LogUsefulException(e); | |||||
Close(); | |||||
} | |||||
} | |||||
private void HandshakeReceive() | |||||
{ | |||||
if (_closed) | |||||
{ | |||||
return; | |||||
} | |||||
try | |||||
{ | |||||
_remote.BeginSend(_firstPacket, 0, _firstPacketLength, 0, StartPipe, null); | |||||
} | |||||
catch (Exception e) | |||||
{ | |||||
logger.LogUsefulException(e); | |||||
Close(); | |||||
} | |||||
} | |||||
private void StartPipe(IAsyncResult ar) | |||||
{ | |||||
if (_closed) | |||||
{ | |||||
return; | |||||
} | |||||
try | |||||
{ | |||||
_remote.EndSend(ar); | |||||
_remote.BeginReceive(remoteRecvBuffer, 0, RecvSize, 0, | |||||
PipeRemoteReceiveCallback, null); | |||||
_local.BeginReceive(connetionRecvBuffer, 0, RecvSize, 0, | |||||
PipeConnectionReceiveCallback, null); | |||||
} | |||||
catch (Exception e) | |||||
{ | |||||
logger.LogUsefulException(e); | |||||
Close(); | |||||
} | |||||
} | |||||
private void PipeRemoteReceiveCallback(IAsyncResult ar) | |||||
{ | |||||
if (_closed) | |||||
{ | |||||
return; | |||||
} | |||||
try | |||||
{ | |||||
int bytesRead = _remote.EndReceive(ar); | |||||
if (bytesRead > 0) | |||||
{ | |||||
_local.BeginSend(remoteRecvBuffer, 0, bytesRead, 0, PipeConnectionSendCallback, null); | |||||
} | |||||
else | |||||
{ | |||||
_local.Shutdown(SocketShutdown.Send); | |||||
_localShutdown = true; | |||||
CheckClose(); | |||||
} | |||||
} | |||||
catch (Exception e) | |||||
{ | |||||
logger.LogUsefulException(e); | |||||
Close(); | |||||
} | |||||
} | |||||
private void PipeConnectionReceiveCallback(IAsyncResult ar) | |||||
{ | |||||
if (_closed) | |||||
{ | |||||
return; | |||||
} | |||||
try | |||||
{ | |||||
int bytesRead = _local.EndReceive(ar); | |||||
if (bytesRead > 0) | |||||
{ | |||||
_remote.BeginSend(connetionRecvBuffer, 0, bytesRead, 0, PipeRemoteSendCallback, null); | |||||
} | |||||
else | |||||
{ | |||||
_remote.Shutdown(SocketShutdown.Send); | |||||
_remoteShutdown = true; | |||||
CheckClose(); | |||||
} | |||||
} | |||||
catch (Exception e) | |||||
{ | |||||
logger.LogUsefulException(e); | |||||
Close(); | |||||
} | |||||
} | |||||
private void PipeRemoteSendCallback(IAsyncResult ar) | |||||
{ | |||||
if (_closed) | |||||
{ | |||||
return; | |||||
} | |||||
try | |||||
{ | |||||
_remote.EndSend(ar); | |||||
_local.BeginReceive(connetionRecvBuffer, 0, RecvSize, 0, | |||||
PipeConnectionReceiveCallback, null); | |||||
} | |||||
catch (Exception e) | |||||
{ | |||||
logger.LogUsefulException(e); | |||||
Close(); | |||||
} | |||||
} | |||||
private void PipeConnectionSendCallback(IAsyncResult ar) | |||||
{ | |||||
if (_closed) | |||||
{ | |||||
return; | |||||
} | |||||
try | |||||
{ | |||||
_local.EndSend(ar); | |||||
_remote.BeginReceive(remoteRecvBuffer, 0, RecvSize, 0, | |||||
PipeRemoteReceiveCallback, null); | |||||
} | |||||
catch (Exception e) | |||||
{ | |||||
logger.LogUsefulException(e); | |||||
Close(); | |||||
} | |||||
} | |||||
private void CheckClose() | |||||
{ | |||||
if (_localShutdown && _remoteShutdown) | |||||
{ | |||||
Close(); | |||||
} | |||||
} | |||||
public void Close() | |||||
{ | |||||
lock (_Lock) | |||||
{ | |||||
if (_closed) | |||||
{ | |||||
return; | |||||
} | |||||
_closed = true; | |||||
} | |||||
if (_local != null) | |||||
{ | |||||
try | |||||
{ | |||||
_local.Shutdown(SocketShutdown.Both); | |||||
_local.Close(); | |||||
} | |||||
catch (Exception e) | |||||
{ | |||||
logger.LogUsefulException(e); | |||||
} | |||||
} | |||||
if (_remote != null) | |||||
{ | |||||
try | |||||
{ | |||||
_remote.Shutdown(SocketShutdown.Both); | |||||
_remote.Dispose(); | |||||
} | |||||
catch (SocketException e) | |||||
{ | |||||
logger.LogUsefulException(e); | |||||
} | |||||
} | |||||
} | |||||
} | |||||
} | |||||
} | |||||
using System; | |||||
using System.Net; | |||||
using System.Net.Sockets; | |||||
using NLog; | |||||
using Shadowsocks.Net; | |||||
namespace Shadowsocks.WPF.Services | |||||
{ | |||||
public class PortForwarder : StreamService | |||||
{ | |||||
private readonly int _targetPort; | |||||
public PortForwarder(int targetPort) | |||||
{ | |||||
_targetPort = targetPort; | |||||
} | |||||
public override bool Handle(CachedNetworkStream stream, object state) | |||||
{ | |||||
byte[] fp = new byte[256]; | |||||
int len = stream.ReadFirstBlock(fp); | |||||
if (stream.Socket.ProtocolType != ProtocolType.Tcp) | |||||
{ | |||||
return false; | |||||
} | |||||
new Handler().Start(fp, len, stream.Socket, _targetPort); | |||||
return true; | |||||
} | |||||
[Obsolete] | |||||
public override bool Handle(byte[] firstPacket, int length, Socket socket, object state) | |||||
{ | |||||
if (socket.ProtocolType != ProtocolType.Tcp) | |||||
{ | |||||
return false; | |||||
} | |||||
new Handler().Start(firstPacket, length, socket, _targetPort); | |||||
return true; | |||||
} | |||||
private class Handler | |||||
{ | |||||
private static Logger logger = LogManager.GetCurrentClassLogger(); | |||||
private byte[] _firstPacket; | |||||
private int _firstPacketLength; | |||||
private Socket _local; | |||||
private Socket _remote; | |||||
private bool _closed = false; | |||||
private bool _localShutdown = false; | |||||
private bool _remoteShutdown = false; | |||||
private const int RecvSize = 2048; | |||||
// remote receive buffer | |||||
private byte[] remoteRecvBuffer = new byte[RecvSize]; | |||||
// connection receive buffer | |||||
private byte[] connetionRecvBuffer = new byte[RecvSize]; | |||||
// instance-based lock | |||||
private readonly object _Lock = new object(); | |||||
public void Start(byte[] firstPacket, int length, Socket socket, int targetPort) | |||||
{ | |||||
_firstPacket = firstPacket; | |||||
_firstPacketLength = length; | |||||
_local = socket; | |||||
try | |||||
{ | |||||
// Local Port Forward use IP as is | |||||
EndPoint remoteEP = new IPEndPoint(_local.AddressFamily == AddressFamily.InterNetworkV6 ? IPAddress.IPv6Loopback : IPAddress.Loopback, targetPort); | |||||
// Connect to the remote endpoint. | |||||
_remote = new Socket(SocketType.Stream, ProtocolType.Tcp); | |||||
_remote.BeginConnect(remoteEP, ConnectCallback, null); | |||||
} | |||||
catch (Exception e) | |||||
{ | |||||
logger.LogUsefulException(e); | |||||
Close(); | |||||
} | |||||
} | |||||
private void ConnectCallback(IAsyncResult ar) | |||||
{ | |||||
if (_closed) | |||||
{ | |||||
return; | |||||
} | |||||
try | |||||
{ | |||||
_remote.EndConnect(ar); | |||||
_remote.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.NoDelay, true); | |||||
HandshakeReceive(); | |||||
} | |||||
catch (Exception e) | |||||
{ | |||||
logger.LogUsefulException(e); | |||||
Close(); | |||||
} | |||||
} | |||||
private void HandshakeReceive() | |||||
{ | |||||
if (_closed) | |||||
{ | |||||
return; | |||||
} | |||||
try | |||||
{ | |||||
_remote.BeginSend(_firstPacket, 0, _firstPacketLength, 0, StartPipe, null); | |||||
} | |||||
catch (Exception e) | |||||
{ | |||||
logger.LogUsefulException(e); | |||||
Close(); | |||||
} | |||||
} | |||||
private void StartPipe(IAsyncResult ar) | |||||
{ | |||||
if (_closed) | |||||
{ | |||||
return; | |||||
} | |||||
try | |||||
{ | |||||
_remote.EndSend(ar); | |||||
_remote.BeginReceive(remoteRecvBuffer, 0, RecvSize, 0, | |||||
PipeRemoteReceiveCallback, null); | |||||
_local.BeginReceive(connetionRecvBuffer, 0, RecvSize, 0, | |||||
PipeConnectionReceiveCallback, null); | |||||
} | |||||
catch (Exception e) | |||||
{ | |||||
logger.LogUsefulException(e); | |||||
Close(); | |||||
} | |||||
} | |||||
private void PipeRemoteReceiveCallback(IAsyncResult ar) | |||||
{ | |||||
if (_closed) | |||||
{ | |||||
return; | |||||
} | |||||
try | |||||
{ | |||||
int bytesRead = _remote.EndReceive(ar); | |||||
if (bytesRead > 0) | |||||
{ | |||||
_local.BeginSend(remoteRecvBuffer, 0, bytesRead, 0, PipeConnectionSendCallback, null); | |||||
} | |||||
else | |||||
{ | |||||
_local.Shutdown(SocketShutdown.Send); | |||||
_localShutdown = true; | |||||
CheckClose(); | |||||
} | |||||
} | |||||
catch (Exception e) | |||||
{ | |||||
logger.LogUsefulException(e); | |||||
Close(); | |||||
} | |||||
} | |||||
private void PipeConnectionReceiveCallback(IAsyncResult ar) | |||||
{ | |||||
if (_closed) | |||||
{ | |||||
return; | |||||
} | |||||
try | |||||
{ | |||||
int bytesRead = _local.EndReceive(ar); | |||||
if (bytesRead > 0) | |||||
{ | |||||
_remote.BeginSend(connetionRecvBuffer, 0, bytesRead, 0, PipeRemoteSendCallback, null); | |||||
} | |||||
else | |||||
{ | |||||
_remote.Shutdown(SocketShutdown.Send); | |||||
_remoteShutdown = true; | |||||
CheckClose(); | |||||
} | |||||
} | |||||
catch (Exception e) | |||||
{ | |||||
logger.LogUsefulException(e); | |||||
Close(); | |||||
} | |||||
} | |||||
private void PipeRemoteSendCallback(IAsyncResult ar) | |||||
{ | |||||
if (_closed) | |||||
{ | |||||
return; | |||||
} | |||||
try | |||||
{ | |||||
_remote.EndSend(ar); | |||||
_local.BeginReceive(connetionRecvBuffer, 0, RecvSize, 0, | |||||
PipeConnectionReceiveCallback, null); | |||||
} | |||||
catch (Exception e) | |||||
{ | |||||
logger.LogUsefulException(e); | |||||
Close(); | |||||
} | |||||
} | |||||
private void PipeConnectionSendCallback(IAsyncResult ar) | |||||
{ | |||||
if (_closed) | |||||
{ | |||||
return; | |||||
} | |||||
try | |||||
{ | |||||
_local.EndSend(ar); | |||||
_remote.BeginReceive(remoteRecvBuffer, 0, RecvSize, 0, | |||||
PipeRemoteReceiveCallback, null); | |||||
} | |||||
catch (Exception e) | |||||
{ | |||||
logger.LogUsefulException(e); | |||||
Close(); | |||||
} | |||||
} | |||||
private void CheckClose() | |||||
{ | |||||
if (_localShutdown && _remoteShutdown) | |||||
{ | |||||
Close(); | |||||
} | |||||
} | |||||
public void Close() | |||||
{ | |||||
lock (_Lock) | |||||
{ | |||||
if (_closed) | |||||
{ | |||||
return; | |||||
} | |||||
_closed = true; | |||||
} | |||||
if (_local != null) | |||||
{ | |||||
try | |||||
{ | |||||
_local.Shutdown(SocketShutdown.Both); | |||||
_local.Close(); | |||||
} | |||||
catch (Exception e) | |||||
{ | |||||
logger.LogUsefulException(e); | |||||
} | |||||
} | |||||
if (_remote != null) | |||||
{ | |||||
try | |||||
{ | |||||
_remote.Shutdown(SocketShutdown.Both); | |||||
_remote.Dispose(); | |||||
} | |||||
catch (SocketException e) | |||||
{ | |||||
logger.LogUsefulException(e); | |||||
} | |||||
} | |||||
} | |||||
} | |||||
} | |||||
} |
@@ -1,170 +1,162 @@ | |||||
using System; | |||||
using System.Diagnostics; | |||||
using System.IO; | |||||
using System.Linq; | |||||
using System.Net; | |||||
using System.Net.Sockets; | |||||
using System.Text; | |||||
using System.Windows.Forms; | |||||
using NLog; | |||||
using Shadowsocks.Model; | |||||
using Shadowsocks.Properties; | |||||
using Shadowsocks.Util; | |||||
using Shadowsocks.Util.ProcessManagement; | |||||
namespace Shadowsocks.Controller | |||||
{ | |||||
class PrivoxyRunner | |||||
{ | |||||
private static Logger logger = LogManager.GetCurrentClassLogger(); | |||||
private static int _uid; | |||||
private static string _uniqueConfigFile; | |||||
private static Job _privoxyJob; | |||||
private Process _process; | |||||
private int _runningPort; | |||||
static PrivoxyRunner() | |||||
{ | |||||
try | |||||
{ | |||||
_uid = Program.WorkingDirectory.GetHashCode(); // Currently we use ss's StartupPath to identify different Privoxy instance. | |||||
_uniqueConfigFile = $"privoxy_{_uid}.conf"; | |||||
_privoxyJob = new Job(); | |||||
FileManager.UncompressFile(Utils.GetTempPath("ss_privoxy.exe"), Resources.privoxy_exe); | |||||
} | |||||
catch (IOException e) | |||||
{ | |||||
logger.LogUsefulException(e); | |||||
} | |||||
} | |||||
public int RunningPort => _runningPort; | |||||
public void Start(Configuration configuration) | |||||
{ | |||||
if (_process == null) | |||||
{ | |||||
Process[] existingPrivoxy = Process.GetProcessesByName("ss_privoxy"); | |||||
foreach (Process p in existingPrivoxy.Where(IsChildProcess)) | |||||
{ | |||||
KillProcess(p); | |||||
} | |||||
string privoxyConfig = Resources.privoxy_conf; | |||||
_runningPort = GetFreePort(configuration.isIPv6Enabled); | |||||
privoxyConfig = privoxyConfig.Replace("__SOCKS_PORT__", configuration.localPort.ToString()); | |||||
privoxyConfig = privoxyConfig.Replace("__PRIVOXY_BIND_PORT__", _runningPort.ToString()); | |||||
privoxyConfig = configuration.isIPv6Enabled | |||||
? privoxyConfig.Replace("__PRIVOXY_BIND_IP__", configuration.shareOverLan ? "[::]" : "[::1]") | |||||
.Replace("__SOCKS_HOST__", "[::1]") | |||||
: privoxyConfig.Replace("__PRIVOXY_BIND_IP__", configuration.shareOverLan ? "0.0.0.0" : "127.0.0.1") | |||||
.Replace("__SOCKS_HOST__", "127.0.0.1"); | |||||
FileManager.ByteArrayToFile(Utils.GetTempPath(_uniqueConfigFile), Encoding.UTF8.GetBytes(privoxyConfig)); | |||||
_process = new Process | |||||
{ | |||||
// Configure the process using the StartInfo properties. | |||||
StartInfo = | |||||
{ | |||||
FileName = "ss_privoxy.exe", | |||||
Arguments = _uniqueConfigFile, | |||||
WorkingDirectory = Utils.GetTempPath(), | |||||
WindowStyle = ProcessWindowStyle.Hidden, | |||||
UseShellExecute = true, | |||||
CreateNoWindow = true | |||||
} | |||||
}; | |||||
_process.Start(); | |||||
/* | |||||
* Add this process to job obj associated with this ss process, so that | |||||
* when ss exit unexpectedly, this process will be forced killed by system. | |||||
*/ | |||||
_privoxyJob.AddProcess(_process.Handle); | |||||
} | |||||
} | |||||
public void Stop() | |||||
{ | |||||
if (_process != null) | |||||
{ | |||||
KillProcess(_process); | |||||
_process.Dispose(); | |||||
_process = null; | |||||
} | |||||
} | |||||
private static void KillProcess(Process p) | |||||
{ | |||||
try | |||||
{ | |||||
p.CloseMainWindow(); | |||||
p.WaitForExit(100); | |||||
if (!p.HasExited) | |||||
{ | |||||
p.Kill(); | |||||
p.WaitForExit(); | |||||
} | |||||
} | |||||
catch (Exception e) | |||||
{ | |||||
logger.LogUsefulException(e); | |||||
} | |||||
} | |||||
/* | |||||
* We won't like to kill other ss instances' ss_privoxy.exe. | |||||
* This function will check whether the given process is created | |||||
* by this process by checking the module path or command line. | |||||
* | |||||
* Since it's required to put ss in different dirs to run muti instances, | |||||
* different instance will create their unique "privoxy_UID.conf" where | |||||
* UID is hash of ss's location. | |||||
*/ | |||||
private static bool IsChildProcess(Process process) | |||||
{ | |||||
try | |||||
{ | |||||
/* | |||||
* Under PortableMode, we could identify it by the path of ss_privoxy.exe. | |||||
*/ | |||||
var path = process.MainModule.FileName; | |||||
return Utils.GetTempPath("ss_privoxy.exe").Equals(path); | |||||
} | |||||
catch (Exception ex) | |||||
{ | |||||
/* | |||||
* Sometimes Process.GetProcessesByName will return some processes that | |||||
* are already dead, and that will cause exceptions here. | |||||
* We could simply ignore those exceptions. | |||||
*/ | |||||
logger.LogUsefulException(ex); | |||||
return false; | |||||
} | |||||
} | |||||
private int GetFreePort(bool isIPv6 = false) | |||||
{ | |||||
int defaultPort = 8123; | |||||
try | |||||
{ | |||||
// TCP stack please do me a favor | |||||
TcpListener l = new TcpListener(isIPv6 ? IPAddress.IPv6Loopback : IPAddress.Loopback, 0); | |||||
l.Start(); | |||||
var port = ((IPEndPoint)l.LocalEndpoint).Port; | |||||
l.Stop(); | |||||
return port; | |||||
} | |||||
catch (Exception e) | |||||
{ | |||||
// in case access denied | |||||
logger.LogUsefulException(e); | |||||
return defaultPort; | |||||
} | |||||
} | |||||
} | |||||
} | |||||
using System; | |||||
using System.Diagnostics; | |||||
using System.IO; | |||||
using System.Linq; | |||||
using System.Net; | |||||
using System.Net.Sockets; | |||||
using System.Text; | |||||
using System.Windows.Forms; | |||||
using NLog; | |||||
using Shadowsocks.Model; | |||||
using Shadowsocks.Properties; | |||||
using Shadowsocks.Util; | |||||
using Shadowsocks.Util.ProcessManagement; | |||||
namespace Shadowsocks.WPF.Services | |||||
{ | |||||
class PrivoxyRunner | |||||
{ | |||||
private static Logger logger = LogManager.GetCurrentClassLogger(); | |||||
private static int _uid; | |||||
private static string _uniqueConfigFile; | |||||
private Process _process; | |||||
private int _runningPort; | |||||
static PrivoxyRunner() | |||||
{ | |||||
try | |||||
{ | |||||
_uid = Program.WorkingDirectory.GetHashCode(); // Currently we use ss's StartupPath to identify different Privoxy instance. | |||||
_uniqueConfigFile = $"privoxy_{_uid}.conf"; | |||||
FileManager.UncompressFile(Utils.GetTempPath("ss_privoxy.exe"), Resources.privoxy_exe); | |||||
} | |||||
catch (IOException e) | |||||
{ | |||||
logger.LogUsefulException(e); | |||||
} | |||||
} | |||||
public int RunningPort => _runningPort; | |||||
public void Start(Configuration configuration) | |||||
{ | |||||
if (_process == null) | |||||
{ | |||||
Process[] existingPrivoxy = Process.GetProcessesByName("ss_privoxy"); | |||||
foreach (Process p in existingPrivoxy.Where(IsChildProcess)) | |||||
{ | |||||
KillProcess(p); | |||||
} | |||||
string privoxyConfig = Resources.privoxy_conf; | |||||
_runningPort = GetFreePort(configuration.isIPv6Enabled); | |||||
privoxyConfig = privoxyConfig.Replace("__SOCKS_PORT__", configuration.localPort.ToString()); | |||||
privoxyConfig = privoxyConfig.Replace("__PRIVOXY_BIND_PORT__", _runningPort.ToString()); | |||||
privoxyConfig = configuration.isIPv6Enabled | |||||
? privoxyConfig.Replace("__PRIVOXY_BIND_IP__", configuration.shareOverLan ? "[::]" : "[::1]") | |||||
.Replace("__SOCKS_HOST__", "[::1]") | |||||
: privoxyConfig.Replace("__PRIVOXY_BIND_IP__", configuration.shareOverLan ? "0.0.0.0" : "127.0.0.1") | |||||
.Replace("__SOCKS_HOST__", "127.0.0.1"); | |||||
FileManager.ByteArrayToFile(Utils.GetTempPath(_uniqueConfigFile), Encoding.UTF8.GetBytes(privoxyConfig)); | |||||
_process = new Process | |||||
{ | |||||
// Configure the process using the StartInfo properties. | |||||
StartInfo = | |||||
{ | |||||
FileName = "ss_privoxy.exe", | |||||
Arguments = _uniqueConfigFile, | |||||
WorkingDirectory = Utils.GetTempPath(), | |||||
WindowStyle = ProcessWindowStyle.Hidden, | |||||
UseShellExecute = true, | |||||
CreateNoWindow = true | |||||
} | |||||
}; | |||||
_process.Start(); | |||||
} | |||||
} | |||||
public void Stop() | |||||
{ | |||||
if (_process != null) | |||||
{ | |||||
KillProcess(_process); | |||||
_process.Dispose(); | |||||
_process = null; | |||||
} | |||||
} | |||||
private static void KillProcess(Process p) | |||||
{ | |||||
try | |||||
{ | |||||
p.CloseMainWindow(); | |||||
p.WaitForExit(100); | |||||
if (!p.HasExited) | |||||
{ | |||||
p.Kill(); | |||||
p.WaitForExit(); | |||||
} | |||||
} | |||||
catch (Exception e) | |||||
{ | |||||
logger.LogUsefulException(e); | |||||
} | |||||
} | |||||
/* | |||||
* We won't like to kill other ss instances' ss_privoxy.exe. | |||||
* This function will check whether the given process is created | |||||
* by this process by checking the module path or command line. | |||||
* | |||||
* Since it's required to put ss in different dirs to run muti instances, | |||||
* different instance will create their unique "privoxy_UID.conf" where | |||||
* UID is hash of ss's location. | |||||
*/ | |||||
private static bool IsChildProcess(Process process) | |||||
{ | |||||
try | |||||
{ | |||||
/* | |||||
* Under PortableMode, we could identify it by the path of ss_privoxy.exe. | |||||
*/ | |||||
var path = process.MainModule.FileName; | |||||
return Utils.GetTempPath("ss_privoxy.exe").Equals(path); | |||||
} | |||||
catch (Exception ex) | |||||
{ | |||||
/* | |||||
* Sometimes Process.GetProcessesByName will return some processes that | |||||
* are already dead, and that will cause exceptions here. | |||||
* We could simply ignore those exceptions. | |||||
*/ | |||||
logger.LogUsefulException(ex); | |||||
return false; | |||||
} | |||||
} | |||||
private int GetFreePort(bool isIPv6 = false) | |||||
{ | |||||
int defaultPort = 8123; | |||||
try | |||||
{ | |||||
// TCP stack please do me a favor | |||||
TcpListener l = new TcpListener(isIPv6 ? IPAddress.IPv6Loopback : IPAddress.Loopback, 0); | |||||
l.Start(); | |||||
var port = ((IPEndPoint)l.LocalEndpoint).Port; | |||||
l.Stop(); | |||||
return port; | |||||
} | |||||
catch (Exception e) | |||||
{ | |||||
// in case access denied | |||||
logger.LogUsefulException(e); | |||||
return defaultPort; | |||||
} | |||||
} | |||||
} | |||||
} |
@@ -1,177 +1,174 @@ | |||||
using System; | |||||
using System.Collections.Specialized; | |||||
using System.Diagnostics; | |||||
using System.IO; | |||||
using System.Net; | |||||
using System.Net.Sockets; | |||||
using Shadowsocks.Model; | |||||
using Shadowsocks.Util.ProcessManagement; | |||||
namespace Shadowsocks.Controller.Service | |||||
{ | |||||
// https://github.com/shadowsocks/shadowsocks-org/wiki/Plugin | |||||
public sealed class Sip003Plugin : IDisposable | |||||
{ | |||||
public IPEndPoint LocalEndPoint { get; private set; } | |||||
public int ProcessId => _started ? _pluginProcess.Id : 0; | |||||
private readonly object _startProcessLock = new object(); | |||||
private readonly Job _pluginJob; | |||||
private readonly Process _pluginProcess; | |||||
private bool _started; | |||||
private bool _disposed; | |||||
public static Sip003Plugin CreateIfConfigured(Server server, bool showPluginOutput) | |||||
{ | |||||
if (server == null) | |||||
{ | |||||
throw new ArgumentNullException(nameof(server)); | |||||
} | |||||
if (string.IsNullOrWhiteSpace(server.plugin)) | |||||
{ | |||||
return null; | |||||
} | |||||
return new Sip003Plugin( | |||||
server.plugin, | |||||
server.plugin_opts, | |||||
server.plugin_args, | |||||
server.server, | |||||
server.server_port, | |||||
showPluginOutput); | |||||
} | |||||
private Sip003Plugin(string plugin, string pluginOpts, string pluginArgs, string serverAddress, int serverPort, bool showPluginOutput) | |||||
{ | |||||
if (plugin == null) throw new ArgumentNullException(nameof(plugin)); | |||||
if (string.IsNullOrWhiteSpace(serverAddress)) | |||||
{ | |||||
throw new ArgumentException("Value cannot be null or whitespace.", nameof(serverAddress)); | |||||
} | |||||
if (serverPort <= 0 || serverPort > 65535) | |||||
{ | |||||
throw new ArgumentOutOfRangeException("serverPort"); | |||||
} | |||||
_pluginProcess = new Process | |||||
{ | |||||
StartInfo = new ProcessStartInfo | |||||
{ | |||||
FileName = plugin, | |||||
Arguments = pluginArgs, | |||||
UseShellExecute = false, | |||||
CreateNoWindow = !showPluginOutput, | |||||
ErrorDialog = false, | |||||
WindowStyle = ProcessWindowStyle.Hidden, | |||||
WorkingDirectory = Program.WorkingDirectory ?? Environment.CurrentDirectory, | |||||
Environment = | |||||
{ | |||||
["SS_REMOTE_HOST"] = serverAddress, | |||||
["SS_REMOTE_PORT"] = serverPort.ToString(), | |||||
["SS_PLUGIN_OPTIONS"] = pluginOpts | |||||
} | |||||
} | |||||
}; | |||||
_pluginJob = new Job(); | |||||
} | |||||
public bool StartIfNeeded() | |||||
{ | |||||
if (_disposed) | |||||
{ | |||||
throw new ObjectDisposedException(GetType().FullName); | |||||
} | |||||
lock (_startProcessLock) | |||||
{ | |||||
if (_started && !_pluginProcess.HasExited) | |||||
{ | |||||
return false; | |||||
} | |||||
var localPort = GetNextFreeTcpPort(); | |||||
LocalEndPoint = new IPEndPoint(IPAddress.Loopback, localPort); | |||||
_pluginProcess.StartInfo.Environment["SS_LOCAL_HOST"] = LocalEndPoint.Address.ToString(); | |||||
_pluginProcess.StartInfo.Environment["SS_LOCAL_PORT"] = LocalEndPoint.Port.ToString(); | |||||
_pluginProcess.StartInfo.Arguments = ExpandEnvironmentVariables(_pluginProcess.StartInfo.Arguments, _pluginProcess.StartInfo.EnvironmentVariables); | |||||
try | |||||
{ | |||||
_pluginProcess.Start(); | |||||
} | |||||
catch (System.ComponentModel.Win32Exception ex) | |||||
{ | |||||
// do not use File.Exists(...), it can not handle the scenarios when the plugin file is in system environment path. | |||||
// https://docs.microsoft.com/en-us/windows/win32/seccrypto/common-hresult-values | |||||
//if ((uint)ex.ErrorCode == 0x80004005) | |||||
// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-erref/18d8fbe8-a967-4f1c-ae50-99ca8e491d2d | |||||
if (ex.NativeErrorCode == 0x00000002) | |||||
{ | |||||
throw new FileNotFoundException(I18N.GetString("Cannot find the plugin program file"), _pluginProcess.StartInfo.FileName, ex); | |||||
} | |||||
throw new ApplicationException(I18N.GetString("Plugin Program"), ex); | |||||
} | |||||
_pluginJob.AddProcess(_pluginProcess.Handle); | |||||
_started = true; | |||||
} | |||||
return true; | |||||
} | |||||
public string ExpandEnvironmentVariables(string name, StringDictionary environmentVariables = null) | |||||
{ | |||||
name = name.ToLower(); | |||||
// Expand the environment variables from the new process itself | |||||
if (environmentVariables != null) | |||||
{ | |||||
foreach(string key in environmentVariables.Keys) | |||||
{ | |||||
name = name.Replace($"%{key.ToLower()}%", environmentVariables[key]); | |||||
} | |||||
} | |||||
// Also expand the environment variables from current main process (system) | |||||
name = Environment.ExpandEnvironmentVariables(name); | |||||
return name; | |||||
} | |||||
static int GetNextFreeTcpPort() | |||||
{ | |||||
var l = new TcpListener(IPAddress.Loopback, 0); | |||||
l.Start(); | |||||
int port = ((IPEndPoint)l.LocalEndpoint).Port; | |||||
l.Stop(); | |||||
return port; | |||||
} | |||||
public void Dispose() | |||||
{ | |||||
if (_disposed) | |||||
{ | |||||
return; | |||||
} | |||||
try | |||||
{ | |||||
if (!_pluginProcess.HasExited) | |||||
{ | |||||
_pluginProcess.Kill(); | |||||
_pluginProcess.WaitForExit(); | |||||
} | |||||
} | |||||
catch (Exception) { } | |||||
finally | |||||
{ | |||||
try | |||||
{ | |||||
_pluginProcess.Dispose(); | |||||
_pluginJob.Dispose(); | |||||
} | |||||
catch (Exception) { } | |||||
_disposed = true; | |||||
} | |||||
} | |||||
} | |||||
using System; | |||||
using System.Collections.Specialized; | |||||
using System.Diagnostics; | |||||
using System.IO; | |||||
using System.Net; | |||||
using System.Net.Sockets; | |||||
using Shadowsocks.Model; | |||||
using Shadowsocks.Util.ProcessManagement; | |||||
namespace Shadowsocks.WPF.Services | |||||
{ | |||||
// https://github.com/shadowsocks/shadowsocks-org/wiki/Plugin | |||||
public sealed class Sip003Plugin : IDisposable | |||||
{ | |||||
public IPEndPoint LocalEndPoint { get; private set; } | |||||
public int ProcessId => _started ? _pluginProcess.Id : 0; | |||||
private readonly object _startProcessLock = new object(); | |||||
private readonly Process _pluginProcess; | |||||
private bool _started; | |||||
private bool _disposed; | |||||
public static Sip003Plugin CreateIfConfigured(Server server, bool showPluginOutput) | |||||
{ | |||||
if (server == null) | |||||
{ | |||||
throw new ArgumentNullException(nameof(server)); | |||||
} | |||||
if (string.IsNullOrWhiteSpace(server.plugin)) | |||||
{ | |||||
return null; | |||||
} | |||||
return new Sip003Plugin( | |||||
server.plugin, | |||||
server.plugin_opts, | |||||
server.plugin_args, | |||||
server.server, | |||||
server.server_port, | |||||
showPluginOutput); | |||||
} | |||||
private Sip003Plugin(string plugin, string pluginOpts, string pluginArgs, string serverAddress, int serverPort, bool showPluginOutput) | |||||
{ | |||||
if (plugin == null) throw new ArgumentNullException(nameof(plugin)); | |||||
if (string.IsNullOrWhiteSpace(serverAddress)) | |||||
{ | |||||
throw new ArgumentException("Value cannot be null or whitespace.", nameof(serverAddress)); | |||||
} | |||||
if (serverPort <= 0 || serverPort > 65535) | |||||
{ | |||||
throw new ArgumentOutOfRangeException("serverPort"); | |||||
} | |||||
_pluginProcess = new Process | |||||
{ | |||||
StartInfo = new ProcessStartInfo | |||||
{ | |||||
FileName = plugin, | |||||
Arguments = pluginArgs, | |||||
UseShellExecute = false, | |||||
CreateNoWindow = !showPluginOutput, | |||||
ErrorDialog = false, | |||||
WindowStyle = ProcessWindowStyle.Hidden, | |||||
WorkingDirectory = Program.WorkingDirectory ?? Environment.CurrentDirectory, | |||||
Environment = | |||||
{ | |||||
["SS_REMOTE_HOST"] = serverAddress, | |||||
["SS_REMOTE_PORT"] = serverPort.ToString(), | |||||
["SS_PLUGIN_OPTIONS"] = pluginOpts | |||||
} | |||||
} | |||||
}; | |||||
} | |||||
public bool StartIfNeeded() | |||||
{ | |||||
if (_disposed) | |||||
{ | |||||
throw new ObjectDisposedException(GetType().FullName); | |||||
} | |||||
lock (_startProcessLock) | |||||
{ | |||||
if (_started && !_pluginProcess.HasExited) | |||||
{ | |||||
return false; | |||||
} | |||||
var localPort = GetNextFreeTcpPort(); | |||||
LocalEndPoint = new IPEndPoint(IPAddress.Loopback, localPort); | |||||
_pluginProcess.StartInfo.Environment["SS_LOCAL_HOST"] = LocalEndPoint.Address.ToString(); | |||||
_pluginProcess.StartInfo.Environment["SS_LOCAL_PORT"] = LocalEndPoint.Port.ToString(); | |||||
_pluginProcess.StartInfo.Arguments = ExpandEnvironmentVariables(_pluginProcess.StartInfo.Arguments, _pluginProcess.StartInfo.EnvironmentVariables); | |||||
try | |||||
{ | |||||
_pluginProcess.Start(); | |||||
} | |||||
catch (System.ComponentModel.Win32Exception ex) | |||||
{ | |||||
// do not use File.Exists(...), it can not handle the scenarios when the plugin file is in system environment path. | |||||
// https://docs.microsoft.com/en-us/windows/win32/seccrypto/common-hresult-values | |||||
//if ((uint)ex.ErrorCode == 0x80004005) | |||||
// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-erref/18d8fbe8-a967-4f1c-ae50-99ca8e491d2d | |||||
if (ex.NativeErrorCode == 0x00000002) | |||||
{ | |||||
throw new FileNotFoundException(I18N.GetString("Cannot find the plugin program file"), _pluginProcess.StartInfo.FileName, ex); | |||||
} | |||||
throw new ApplicationException(I18N.GetString("Plugin Program"), ex); | |||||
} | |||||
_pluginJob.AddProcess(_pluginProcess.Handle); | |||||
_started = true; | |||||
} | |||||
return true; | |||||
} | |||||
public string ExpandEnvironmentVariables(string name, StringDictionary environmentVariables = null) | |||||
{ | |||||
name = name.ToLower(); | |||||
// Expand the environment variables from the new process itself | |||||
if (environmentVariables != null) | |||||
{ | |||||
foreach(string key in environmentVariables.Keys) | |||||
{ | |||||
name = name.Replace($"%{key.ToLower()}%", environmentVariables[key]); | |||||
} | |||||
} | |||||
// Also expand the environment variables from current main process (system) | |||||
name = Environment.ExpandEnvironmentVariables(name); | |||||
return name; | |||||
} | |||||
static int GetNextFreeTcpPort() | |||||
{ | |||||
var l = new TcpListener(IPAddress.Loopback, 0); | |||||
l.Start(); | |||||
int port = ((IPEndPoint)l.LocalEndpoint).Port; | |||||
l.Stop(); | |||||
return port; | |||||
} | |||||
public void Dispose() | |||||
{ | |||||
if (_disposed) | |||||
{ | |||||
return; | |||||
} | |||||
try | |||||
{ | |||||
if (!_pluginProcess.HasExited) | |||||
{ | |||||
_pluginProcess.Kill(); | |||||
_pluginProcess.WaitForExit(); | |||||
} | |||||
} | |||||
catch (Exception) { } | |||||
finally | |||||
{ | |||||
try | |||||
{ | |||||
_pluginProcess.Dispose(); | |||||
_pluginJob.Dispose(); | |||||
} | |||||
catch (Exception) { } | |||||
_disposed = true; | |||||
} | |||||
} | |||||
} | |||||
} | } |
@@ -1,20 +1,19 @@ | |||||
using System; | |||||
using System; | |||||
using System.Collections.Generic; | using System.Collections.Generic; | ||||
using System.Linq; | using System.Linq; | ||||
using System.Runtime.InteropServices; | using System.Runtime.InteropServices; | ||||
using System.Text; | using System.Text; | ||||
namespace Shadowsocks.Util.SystemProxy | |||||
namespace Shadowsocks.WPF.Services.SystemProxy | |||||
{ | { | ||||
enum RasFieldSizeConst | |||||
public enum RasFieldSizeConst | |||||
{ | { | ||||
MaxEntryName = 256, | MaxEntryName = 256, | ||||
MaxPath = 260, | MaxPath = 260, | ||||
} | } | ||||
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] | [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] | ||||
struct RasEntryName | |||||
public struct RasEntryName | |||||
{ | { | ||||
public int dwSize; | public int dwSize; | ||||
@@ -26,7 +25,8 @@ namespace Shadowsocks.Util.SystemProxy | |||||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = RAS.MaxPath + 1)] | [MarshalAs(UnmanagedType.ByValTStr, SizeConst = RAS.MaxPath + 1)] | ||||
public string szPhonebookPath; | public string szPhonebookPath; | ||||
} | } | ||||
class RAS | |||||
public class RAS | |||||
{ | { | ||||
public const int MaxEntryName = 256; | public const int MaxEntryName = 256; | ||||
public const int MaxPath = 260; | public const int MaxPath = 260; |
@@ -1,11 +1,11 @@ | |||||
using System; | |||||
using System; | |||||
using System.Collections.Generic; | using System.Collections.Generic; | ||||
using System.ComponentModel; | using System.ComponentModel; | ||||
using System.Linq; | using System.Linq; | ||||
using System.Runtime.InteropServices; | using System.Runtime.InteropServices; | ||||
using NLog; | using NLog; | ||||
namespace Shadowsocks.Util.SystemProxy | |||||
namespace Shadowsocks.WPF.Services.SystemProxy | |||||
{ | { | ||||
#region Windows API data structure definition | #region Windows API data structure definition | ||||
public enum InternetOptions | public enum InternetOptions |
@@ -1,178 +1,178 @@ | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.Diagnostics; | |||||
using System.IO; | |||||
using System.Net; | |||||
using System.Net.Http; | |||||
using System.Reflection; | |||||
using System.Text.RegularExpressions; | |||||
using System.Threading.Tasks; | |||||
using System.Windows; | |||||
using Newtonsoft.Json.Linq; | |||||
using NLog; | |||||
using Shadowsocks.Localization; | |||||
using Shadowsocks.Model; | |||||
using Shadowsocks.Util; | |||||
using Shadowsocks.Views; | |||||
namespace Shadowsocks.Controller | |||||
{ | |||||
public class UpdateChecker | |||||
{ | |||||
private readonly Logger logger; | |||||
private readonly HttpClient httpClient; | |||||
// https://developer.github.com/v3/repos/releases/ | |||||
private const string UpdateURL = "https://api.github.com/repos/shadowsocks/shadowsocks-windows/releases"; | |||||
private Configuration _config; | |||||
private Window versionUpdatePromptWindow; | |||||
private JToken _releaseObject; | |||||
public string NewReleaseVersion { get; private set; } | |||||
public string NewReleaseZipFilename { get; private set; } | |||||
public event EventHandler CheckUpdateCompleted; | |||||
public static readonly string Version = Assembly.GetEntryAssembly().GetName().Version.ToString(); | |||||
private readonly Version _version; | |||||
public UpdateChecker() | |||||
{ | |||||
logger = LogManager.GetCurrentClassLogger(); | |||||
httpClient = Program.MainController.GetHttpClient(); | |||||
_version = new Version(Version); | |||||
_config = Program.MainController.GetCurrentConfiguration(); | |||||
} | |||||
/// <summary> | |||||
/// Checks for updates and asks the user if updates are found. | |||||
/// </summary> | |||||
/// <param name="millisecondsDelay">A delay in milliseconds before checking.</param> | |||||
/// <returns></returns> | |||||
public async Task CheckForVersionUpdate(int millisecondsDelay = 0) | |||||
{ | |||||
// delay | |||||
logger.Info($"Waiting for {millisecondsDelay}ms before checking for version update."); | |||||
await Task.Delay(millisecondsDelay); | |||||
// update _config so we would know if the user checked or unchecked pre-release checks | |||||
_config = Program.MainController.GetCurrentConfiguration(); | |||||
// start | |||||
logger.Info($"Checking for version update."); | |||||
try | |||||
{ | |||||
// list releases via API | |||||
var releasesListJsonString = await httpClient.GetStringAsync(UpdateURL); | |||||
// parse | |||||
var releasesJArray = JArray.Parse(releasesListJsonString); | |||||
foreach (var releaseObject in releasesJArray) | |||||
{ | |||||
var releaseTagName = (string)releaseObject["tag_name"]; | |||||
var releaseVersion = new Version(releaseTagName); | |||||
if (releaseTagName == _config.skippedUpdateVersion) // finished checking | |||||
break; | |||||
if (releaseVersion.CompareTo(_version) > 0 && | |||||
(!(bool)releaseObject["prerelease"] || _config.checkPreRelease && (bool)releaseObject["prerelease"])) // selected | |||||
{ | |||||
logger.Info($"Found new version {releaseTagName}."); | |||||
_releaseObject = releaseObject; | |||||
NewReleaseVersion = releaseTagName; | |||||
AskToUpdate(releaseObject); | |||||
return; | |||||
} | |||||
} | |||||
logger.Info($"No new versions found."); | |||||
CheckUpdateCompleted?.Invoke(this, new EventArgs()); | |||||
} | |||||
catch (Exception e) | |||||
{ | |||||
logger.LogUsefulException(e); | |||||
} | |||||
} | |||||
/// <summary> | |||||
/// Opens a window to show the update's information. | |||||
/// </summary> | |||||
/// <param name="releaseObject">The update release object.</param> | |||||
private void AskToUpdate(JToken releaseObject) | |||||
{ | |||||
if (versionUpdatePromptWindow == null) | |||||
{ | |||||
versionUpdatePromptWindow = new Window() | |||||
{ | |||||
Title = LocalizationProvider.GetLocalizedValue<string>("VersionUpdate"), | |||||
Height = 480, | |||||
Width = 640, | |||||
MinHeight = 480, | |||||
MinWidth = 640, | |||||
Content = new VersionUpdatePromptView(releaseObject) | |||||
}; | |||||
versionUpdatePromptWindow.Closed += VersionUpdatePromptWindow_Closed; | |||||
versionUpdatePromptWindow.Show(); | |||||
} | |||||
versionUpdatePromptWindow.Activate(); | |||||
} | |||||
private void VersionUpdatePromptWindow_Closed(object sender, EventArgs e) | |||||
{ | |||||
versionUpdatePromptWindow = null; | |||||
} | |||||
/// <summary> | |||||
/// Downloads the selected update and notifies the user. | |||||
/// </summary> | |||||
/// <returns></returns> | |||||
public async Task DoUpdate() | |||||
{ | |||||
try | |||||
{ | |||||
var assets = (JArray)_releaseObject["assets"]; | |||||
// download all assets | |||||
foreach (JObject asset in assets) | |||||
{ | |||||
var filename = (string)asset["name"]; | |||||
var browser_download_url = (string)asset["browser_download_url"]; | |||||
var response = await httpClient.GetAsync(browser_download_url); | |||||
using (var downloadedFileStream = File.Create(Utils.GetTempPath(filename))) | |||||
await response.Content.CopyToAsync(downloadedFileStream); | |||||
logger.Info($"Downloaded {filename}."); | |||||
// store .zip filename | |||||
if (filename.EndsWith(".zip")) | |||||
NewReleaseZipFilename = filename; | |||||
} | |||||
logger.Info("Finished downloading."); | |||||
// notify user | |||||
CloseVersionUpdatePromptWindow(); | |||||
Process.Start("explorer.exe", $"/select, \"{Utils.GetTempPath(NewReleaseZipFilename)}\""); | |||||
} | |||||
catch (Exception e) | |||||
{ | |||||
logger.LogUsefulException(e); | |||||
} | |||||
} | |||||
/// <summary> | |||||
/// Saves the skipped update version. | |||||
/// </summary> | |||||
public void SkipUpdate() | |||||
{ | |||||
var version = (string)_releaseObject["tag_name"] ?? ""; | |||||
_config.skippedUpdateVersion = version; | |||||
Program.MainController.SaveSkippedUpdateVerion(version); | |||||
logger.Info($"The update {version} has been skipped and will be ignored next time."); | |||||
CloseVersionUpdatePromptWindow(); | |||||
} | |||||
/// <summary> | |||||
/// Closes the update prompt window. | |||||
/// </summary> | |||||
public void CloseVersionUpdatePromptWindow() | |||||
{ | |||||
if (versionUpdatePromptWindow != null) | |||||
{ | |||||
versionUpdatePromptWindow.Close(); | |||||
versionUpdatePromptWindow = null; | |||||
} | |||||
} | |||||
} | |||||
} | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.Diagnostics; | |||||
using System.IO; | |||||
using System.Net; | |||||
using System.Net.Http; | |||||
using System.Reflection; | |||||
using System.Text.RegularExpressions; | |||||
using System.Threading.Tasks; | |||||
using System.Windows; | |||||
using Newtonsoft.Json.Linq; | |||||
using NLog; | |||||
using Shadowsocks.Localization; | |||||
using Shadowsocks.Model; | |||||
using Shadowsocks.Util; | |||||
using Shadowsocks.Views; | |||||
namespace Shadowsocks.WPF.Services | |||||
{ | |||||
public class UpdateChecker | |||||
{ | |||||
private readonly Logger logger; | |||||
private readonly HttpClient httpClient; | |||||
// https://developer.github.com/v3/repos/releases/ | |||||
private const string UpdateURL = "https://api.github.com/repos/shadowsocks/shadowsocks-windows/releases"; | |||||
private Configuration _config; | |||||
private Window versionUpdatePromptWindow; | |||||
private JToken _releaseObject; | |||||
public string NewReleaseVersion { get; private set; } | |||||
public string NewReleaseZipFilename { get; private set; } | |||||
public event EventHandler CheckUpdateCompleted; | |||||
public static readonly string Version = Assembly.GetEntryAssembly().GetName().Version.ToString(); | |||||
private readonly Version _version; | |||||
public UpdateChecker() | |||||
{ | |||||
logger = LogManager.GetCurrentClassLogger(); | |||||
httpClient = Program.MainController.GetHttpClient(); | |||||
_version = new Version(Version); | |||||
_config = Program.MainController.GetCurrentConfiguration(); | |||||
} | |||||
/// <summary> | |||||
/// Checks for updates and asks the user if updates are found. | |||||
/// </summary> | |||||
/// <param name="millisecondsDelay">A delay in milliseconds before checking.</param> | |||||
/// <returns></returns> | |||||
public async Task CheckForVersionUpdate(int millisecondsDelay = 0) | |||||
{ | |||||
// delay | |||||
logger.Info($"Waiting for {millisecondsDelay}ms before checking for version update."); | |||||
await Task.Delay(millisecondsDelay); | |||||
// update _config so we would know if the user checked or unchecked pre-release checks | |||||
_config = Program.MainController.GetCurrentConfiguration(); | |||||
// start | |||||
logger.Info($"Checking for version update."); | |||||
try | |||||
{ | |||||
// list releases via API | |||||
var releasesListJsonString = await httpClient.GetStringAsync(UpdateURL); | |||||
// parse | |||||
var releasesJArray = JArray.Parse(releasesListJsonString); | |||||
foreach (var releaseObject in releasesJArray) | |||||
{ | |||||
var releaseTagName = (string)releaseObject["tag_name"]; | |||||
var releaseVersion = new Version(releaseTagName); | |||||
if (releaseTagName == _config.skippedUpdateVersion) // finished checking | |||||
break; | |||||
if (releaseVersion.CompareTo(_version) > 0 && | |||||
(!(bool)releaseObject["prerelease"] || _config.checkPreRelease && (bool)releaseObject["prerelease"])) // selected | |||||
{ | |||||
logger.Info($"Found new version {releaseTagName}."); | |||||
_releaseObject = releaseObject; | |||||
NewReleaseVersion = releaseTagName; | |||||
AskToUpdate(releaseObject); | |||||
return; | |||||
} | |||||
} | |||||
logger.Info($"No new versions found."); | |||||
CheckUpdateCompleted?.Invoke(this, new EventArgs()); | |||||
} | |||||
catch (Exception e) | |||||
{ | |||||
logger.LogUsefulException(e); | |||||
} | |||||
} | |||||
/// <summary> | |||||
/// Opens a window to show the update's information. | |||||
/// </summary> | |||||
/// <param name="releaseObject">The update release object.</param> | |||||
private void AskToUpdate(JToken releaseObject) | |||||
{ | |||||
if (versionUpdatePromptWindow == null) | |||||
{ | |||||
versionUpdatePromptWindow = new Window() | |||||
{ | |||||
Title = LocalizationProvider.GetLocalizedValue<string>("VersionUpdate"), | |||||
Height = 480, | |||||
Width = 640, | |||||
MinHeight = 480, | |||||
MinWidth = 640, | |||||
Content = new VersionUpdatePromptView(releaseObject) | |||||
}; | |||||
versionUpdatePromptWindow.Closed += VersionUpdatePromptWindow_Closed; | |||||
versionUpdatePromptWindow.Show(); | |||||
} | |||||
versionUpdatePromptWindow.Activate(); | |||||
} | |||||
private void VersionUpdatePromptWindow_Closed(object sender, EventArgs e) | |||||
{ | |||||
versionUpdatePromptWindow = null; | |||||
} | |||||
/// <summary> | |||||
/// Downloads the selected update and notifies the user. | |||||
/// </summary> | |||||
/// <returns></returns> | |||||
public async Task DoUpdate() | |||||
{ | |||||
try | |||||
{ | |||||
var assets = (JArray)_releaseObject["assets"]; | |||||
// download all assets | |||||
foreach (JObject asset in assets) | |||||
{ | |||||
var filename = (string)asset["name"]; | |||||
var browser_download_url = (string)asset["browser_download_url"]; | |||||
var response = await httpClient.GetAsync(browser_download_url); | |||||
using (var downloadedFileStream = File.Create(Utils.GetTempPath(filename))) | |||||
await response.Content.CopyToAsync(downloadedFileStream); | |||||
logger.Info($"Downloaded {filename}."); | |||||
// store .zip filename | |||||
if (filename.EndsWith(".zip")) | |||||
NewReleaseZipFilename = filename; | |||||
} | |||||
logger.Info("Finished downloading."); | |||||
// notify user | |||||
CloseVersionUpdatePromptWindow(); | |||||
Process.Start("explorer.exe", $"/select, \"{Utils.GetTempPath(NewReleaseZipFilename)}\""); | |||||
} | |||||
catch (Exception e) | |||||
{ | |||||
logger.LogUsefulException(e); | |||||
} | |||||
} | |||||
/// <summary> | |||||
/// Saves the skipped update version. | |||||
/// </summary> | |||||
public void SkipUpdate() | |||||
{ | |||||
var version = (string)_releaseObject["tag_name"] ?? ""; | |||||
_config.skippedUpdateVersion = version; | |||||
Program.MainController.SaveSkippedUpdateVerion(version); | |||||
logger.Info($"The update {version} has been skipped and will be ignored next time."); | |||||
CloseVersionUpdatePromptWindow(); | |||||
} | |||||
/// <summary> | |||||
/// Closes the update prompt window. | |||||
/// </summary> | |||||
public void CloseVersionUpdatePromptWindow() | |||||
{ | |||||
if (versionUpdatePromptWindow != null) | |||||
{ | |||||
versionUpdatePromptWindow.Close(); | |||||
versionUpdatePromptWindow = null; | |||||
} | |||||
} | |||||
} | |||||
} |
@@ -20,25 +20,11 @@ | |||||
<PackageReference Include="ReactiveUI.Fody" Version="12.1.1" /> | <PackageReference Include="ReactiveUI.Fody" Version="12.1.1" /> | ||||
<PackageReference Include="ReactiveUI.Validation" Version="1.8.6" /> | <PackageReference Include="ReactiveUI.Validation" Version="1.8.6" /> | ||||
<PackageReference Include="ReactiveUI.WPF" Version="12.1.1" /> | <PackageReference Include="ReactiveUI.WPF" Version="12.1.1" /> | ||||
<PackageReference Include="System.CommandLine" Version="2.0.0-beta1.20371.2" /> | |||||
<PackageReference Include="WPFLocalizeExtension" Version="3.8.0" /> | <PackageReference Include="WPFLocalizeExtension" Version="3.8.0" /> | ||||
<PackageReference Include="ZXing.Net" Version="0.16.6" /> | <PackageReference Include="ZXing.Net" Version="0.16.6" /> | ||||
</ItemGroup> | </ItemGroup> | ||||
<ItemGroup> | |||||
<ProjectReference Include="..\Shadowsocks\Shadowsocks.csproj" /> | |||||
</ItemGroup> | |||||
<ItemGroup> | |||||
<Resource Include="Assets\ss128.pdn" /> | |||||
<Resource Include="Assets\ss32.pdn" /> | |||||
<Resource Include="Assets\ss32Fill.png" /> | |||||
<Resource Include="Assets\ss32In.png" /> | |||||
<Resource Include="Assets\ss32Out.png" /> | |||||
<Resource Include="Assets\ss32Outline.png" /> | |||||
<Resource Include="Assets\ssw128.png" /> | |||||
<Resource Include="Assets\shadowsocks.ico" /> | |||||
</ItemGroup> | |||||
<ItemGroup> | <ItemGroup> | ||||
<Compile Update="Localization\Strings.Designer.cs"> | <Compile Update="Localization\Strings.Designer.cs"> | ||||
<DesignTime>True</DesignTime> | <DesignTime>True</DesignTime> | ||||
@@ -58,4 +44,9 @@ | |||||
<Folder Include="Resources\" /> | <Folder Include="Resources\" /> | ||||
</ItemGroup> | </ItemGroup> | ||||
<ItemGroup> | |||||
<ProjectReference Include="..\Shadowsocks.Net\Shadowsocks.Net.csproj" /> | |||||
<ProjectReference Include="..\Shadowsocks\Shadowsocks.csproj" /> | |||||
</ItemGroup> | |||||
</Project> | </Project> |
@@ -0,0 +1,15 @@ | |||||
using ReactiveUI; | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.Text; | |||||
namespace Shadowsocks.WPF.ViewModels | |||||
{ | |||||
public class DashboardViewModel : ReactiveObject | |||||
{ | |||||
public DashboardViewModel() | |||||
{ | |||||
} | |||||
} | |||||
} |
@@ -1,7 +1,4 @@ | |||||
using ReactiveUI; | using ReactiveUI; | ||||
using Shadowsocks.Controller.Service; | |||||
using System; | |||||
namespace Shadowsocks.WPF.ViewModels | namespace Shadowsocks.WPF.ViewModels | ||||
{ | { | ||||
@@ -1,9 +1,9 @@ | |||||
using ReactiveUI; | |||||
using ReactiveUI; | |||||
using ReactiveUI.Fody.Helpers; | using ReactiveUI.Fody.Helpers; | ||||
using ReactiveUI.Validation.Extensions; | using ReactiveUI.Validation.Extensions; | ||||
using ReactiveUI.Validation.Helpers; | using ReactiveUI.Validation.Helpers; | ||||
using Shadowsocks.Controller; | using Shadowsocks.Controller; | ||||
using Shadowsocks.Localization; | |||||
using Shadowsocks.WPF.Localization; | |||||
using Shadowsocks.Model; | using Shadowsocks.Model; | ||||
using Shadowsocks.View; | using Shadowsocks.View; | ||||
using System; | using System; | ||||
@@ -0,0 +1,15 @@ | |||||
using ReactiveUI; | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.Text; | |||||
namespace Shadowsocks.WPF.ViewModels | |||||
{ | |||||
public class RoutingViewModel : ReactiveObject | |||||
{ | |||||
public RoutingViewModel() | |||||
{ | |||||
} | |||||
} | |||||
} |
@@ -0,0 +1,15 @@ | |||||
using ReactiveUI; | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.Text; | |||||
namespace Shadowsocks.WPF.ViewModels | |||||
{ | |||||
public class ServersViewModel : ReactiveObject | |||||
{ | |||||
public ServersViewModel() | |||||
{ | |||||
} | |||||
} | |||||
} |
@@ -0,0 +1,15 @@ | |||||
using ReactiveUI; | |||||
using System; | |||||
using System.Collections.Generic; | |||||
using System.Text; | |||||
namespace Shadowsocks.WPF.ViewModels | |||||
{ | |||||
public class SettingsViewModel : ReactiveObject | |||||
{ | |||||
public SettingsViewModel() | |||||
{ | |||||
} | |||||
} | |||||
} |
@@ -0,0 +1,27 @@ | |||||
<reactiveui:ReactiveUserControl | |||||
x:Class="Shadowsocks.WPF.Views.DashboardView" | |||||
x:TypeArguments="vms:DashboardViewModel" | |||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" | |||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" | |||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" | |||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" | |||||
xmlns:local="clr-namespace:Shadowsocks.WPF.Views" | |||||
xmlns:vms="clr-namespace:Shadowsocks.WPF.ViewModels" | |||||
xmlns:reactiveui="http://reactiveui.net" | |||||
xmlns:lex="http://wpflocalizeextension.codeplex.com" | |||||
lex:LocalizeDictionary.DesignCulture="en" | |||||
lex:ResxLocalizationProvider.DefaultAssembly="Shadowsocks" | |||||
lex:ResxLocalizationProvider.DefaultDictionary="Strings" | |||||
mc:Ignorable="d" | |||||
d:DesignHeight="450" d:DesignWidth="800"> | |||||
<Grid Margin="8"> | |||||
<Grid.RowDefinitions> | |||||
<RowDefinition Height="Auto" /> | |||||
<RowDefinition Height="Auto" /> | |||||
</Grid.RowDefinitions> | |||||
<Grid.ColumnDefinitions> | |||||
<ColumnDefinition Width="Auto" /> | |||||
<ColumnDefinition Width="Auto" /> | |||||
</Grid.ColumnDefinitions> | |||||
</Grid> | |||||
</reactiveui:ReactiveUserControl> |