- 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.IO; | |||
using System.Net.Sockets; | |||
@@ -7,7 +7,7 @@ using System.Text; | |||
using System.Threading; | |||
using System.Threading.Tasks; | |||
namespace Shadowsocks.Controller | |||
namespace Shadowsocks.Net | |||
{ | |||
// cache first packet for duty-chain pattern listener | |||
public class CachedNetworkStream : Stream |
@@ -2,7 +2,7 @@ using System; | |||
using System.Collections.Generic; | |||
using System.Security.Cryptography; | |||
namespace Shadowsocks.Crypto.AEAD | |||
namespace Shadowsocks.Net.Crypto.AEAD | |||
{ | |||
public class AEADAesGcmNativeCrypto : AEADCrypto | |||
{ |
@@ -1,11 +1,10 @@ | |||
using System; | |||
using System.Collections.Generic; | |||
using Org.BouncyCastle.Crypto.Engines; | |||
using Org.BouncyCastle.Crypto.Modes; | |||
using Org.BouncyCastle.Crypto.Parameters; | |||
using System; | |||
using System.Collections.Generic; | |||
namespace Shadowsocks.Crypto.AEAD | |||
namespace Shadowsocks.Net.Crypto.AEAD | |||
{ | |||
public class AEADBouncyCastleCrypto : AEADCrypto | |||
{ |
@@ -1,15 +1,12 @@ | |||
using Shadowsocks.Net.Crypto.Exception; | |||
using Shadowsocks.Net.Crypto.Stream; | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Net; | |||
using System.Runtime.CompilerServices; | |||
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 | |||
{ |
@@ -1,10 +1,9 @@ | |||
using System; | |||
using System.Collections.Generic; | |||
using NaCl.Core; | |||
using NaCl.Core.Base; | |||
using System; | |||
using System.Collections.Generic; | |||
namespace Shadowsocks.Crypto.AEAD | |||
namespace Shadowsocks.Net.Crypto.AEAD | |||
{ | |||
public class AEADNaClCrypto : AEADCrypto | |||
{ |
@@ -1,4 +1,4 @@ | |||
namespace Shadowsocks.Crypto | |||
namespace Shadowsocks.Net.Crypto | |||
{ | |||
public enum CipherFamily | |||
{ |
@@ -1,6 +1,6 @@ | |||
using System; | |||
namespace Shadowsocks.Crypto | |||
namespace Shadowsocks.Net.Crypto | |||
{ | |||
public abstract class CryptoBase : ICrypto | |||
{ |
@@ -3,10 +3,10 @@ using System.Collections.Generic; | |||
using System.Reflection; | |||
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 | |||
{ |
@@ -6,7 +6,7 @@ using System; | |||
using System.Security.Cryptography; | |||
using System.Threading; | |||
namespace Shadowsocks.Crypto | |||
namespace Shadowsocks.Net.Crypto | |||
{ | |||
public static class CryptoUtils | |||
{ |
@@ -1,4 +1,4 @@ | |||
namespace Shadowsocks.Crypto.Exception | |||
namespace Shadowsocks.Net.Crypto.Exception | |||
{ | |||
public class CryptoErrorException : System.Exception | |||
{ |
@@ -1,6 +1,6 @@ | |||
using System; | |||
namespace Shadowsocks.Crypto | |||
namespace Shadowsocks.Net.Crypto | |||
{ | |||
public interface ICrypto | |||
{ |
@@ -1,7 +1,7 @@ | |||
using System; | |||
using System.Security.Cryptography; | |||
namespace Shadowsocks.Crypto | |||
namespace Shadowsocks.Net.Crypto | |||
{ | |||
public static class RNG | |||
{ |
@@ -5,7 +5,7 @@ using System; | |||
using System.Collections.Generic; | |||
using System.Runtime.CompilerServices; | |||
namespace Shadowsocks.Crypto.Stream | |||
namespace Shadowsocks.Net.Crypto.Stream | |||
{ | |||
public class StreamAesCfbBouncyCastleCrypto : StreamCrypto |
@@ -3,7 +3,7 @@ using System.Collections.Generic; | |||
using System.Runtime.CompilerServices; | |||
using NaCl.Core; | |||
namespace Shadowsocks.Crypto.Stream | |||
namespace Shadowsocks.Net.Crypto.Stream | |||
{ | |||
public class StreamChachaNaClCrypto : StreamCrypto | |||
{ |
@@ -1,11 +1,9 @@ | |||
using NLog; | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Runtime.CompilerServices; | |||
using System.Text; | |||
namespace Shadowsocks.Crypto.Stream | |||
namespace Shadowsocks.Net.Crypto.Stream | |||
{ | |||
public abstract class StreamCrypto : CryptoBase | |||
{ |
@@ -1,7 +1,7 @@ | |||
using System; | |||
using System.Collections.Generic; | |||
namespace Shadowsocks.Crypto.Stream | |||
namespace Shadowsocks.Net.Crypto.Stream | |||
{ | |||
public class StreamPlainNativeCrypto : StreamCrypto | |||
{ |
@@ -2,7 +2,7 @@ using System; | |||
using System.Collections.Generic; | |||
using System.Runtime.CompilerServices; | |||
namespace Shadowsocks.Crypto.Stream | |||
namespace Shadowsocks.Net.Crypto.Stream | |||
{ | |||
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 | |||
{ |
@@ -1,11 +1,10 @@ | |||
using System; | |||
using System; | |||
using System.Net; | |||
using System.Net.Sockets; | |||
using System.Threading; | |||
using System.Threading.Tasks; | |||
using Shadowsocks.Util.Sockets; | |||
namespace Shadowsocks.Proxy | |||
namespace Shadowsocks.Net.Proxy | |||
{ | |||
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.Sockets; | |||
using System.Threading; | |||
using System.Threading.Tasks; | |||
namespace Shadowsocks.Proxy | |||
namespace Shadowsocks.Net.Proxy | |||
{ | |||
public interface IProxy | |||
{ | |||
EndPoint LocalEndPoint { get; } |
@@ -1,9 +1,9 @@ | |||
using System; | |||
using System; | |||
using System.Net.Sockets; | |||
using System.Text; | |||
using System.Threading.Tasks; | |||
namespace Shadowsocks.Util.Sockets | |||
namespace Shadowsocks.Net.Proxy | |||
{ | |||
public class LineReader | |||
{ |
@@ -1,13 +1,11 @@ | |||
using System; | |||
using System; | |||
using System.Net; | |||
using System.Net.Sockets; | |||
using System.Text; | |||
using System.Threading; | |||
using System.Threading.Tasks; | |||
using Shadowsocks.Controller; | |||
using Shadowsocks.Util.Sockets; | |||
namespace Shadowsocks.Proxy | |||
namespace Shadowsocks.Net.Proxy | |||
{ | |||
public class Socks5Proxy : IProxy | |||
{ | |||
@@ -37,11 +35,11 @@ namespace Shadowsocks.Proxy | |||
await _remote.SendAsync(new byte[] { 5, 1, 0 }, SocketFlags.None); | |||
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) | |||
{ | |||
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 | |||
break; | |||
default: | |||
throw new Exception(I18N.GetString("Proxy request failed")); | |||
throw new Exception("Proxy request failed"); | |||
} | |||
port = ((IPEndPoint)DestEndPoint).Port; | |||
var addr = ((IPEndPoint)DestEndPoint).Address.GetAddressBytes(); | |||
Array.Copy(addr, 0, request, 4, request.Length - 4 - 2); | |||
} | |||
// 构造request包剩余部分 | |||
request[0] = 5; | |||
request[1] = 1; | |||
request[2] = 0; | |||
@@ -99,11 +96,11 @@ namespace Shadowsocks.Proxy | |||
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) | |||
{ | |||
throw new Exception(I18N.GetString("Proxy request failed")); | |||
throw new Exception("Proxy request failed"); | |||
} | |||
var addrLen = _receiveBuffer[3] switch | |||
{ | |||
@@ -113,11 +110,8 @@ namespace Shadowsocks.Proxy | |||
}; | |||
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) |
@@ -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; | |||
namespace Shadowsocks.Common.SystemProxy | |||
namespace Shadowsocks.Net.SystemProxy | |||
{ | |||
enum ProxyExceptionType | |||
public enum ProxyExceptionType | |||
{ | |||
Unspecific, | |||
FailToRun, | |||
@@ -12,7 +12,7 @@ namespace Shadowsocks.Common.SystemProxy | |||
QueryReturnMalformed | |||
} | |||
class ProxyException : Exception | |||
public class ProxyException : Exception | |||
{ | |||
// provide more specific information about exception | |||
public ProxyExceptionType Type { get; } |
@@ -1,5 +1,4 @@ | |||
using NLog; | |||
using Shadowsocks.Model; | |||
using NLog; | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
@@ -7,7 +6,7 @@ using System.Net; | |||
using System.Net.NetworkInformation; | |||
using System.Net.Sockets; | |||
namespace Shadowsocks.Controller | |||
namespace Shadowsocks.Net | |||
{ | |||
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.Util; | |||
using System; | |||
@@ -13,7 +13,7 @@ using System.Net.Http; | |||
using System.Threading.Tasks; | |||
using System.Security.Cryptography; | |||
namespace Shadowsocks.Controller | |||
namespace Shadowsocks.PAC | |||
{ | |||
public class GeositeResultEventArgs : EventArgs | |||
{ | |||
@@ -21,7 +21,7 @@ namespace Shadowsocks.Controller | |||
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:x="http://schemas.microsoft.com/winfx/2006/xaml" | |||
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"> | |||
<Application.Resources> | |||
<ResourceDictionary> | |||
<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.MergedDictionaries> | |||
</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 Shadowsocks.Controller.Hotkeys; | |||
using Shadowsocks.Model; | |||
using System; | |||
using System.Windows.Forms; | |||
namespace Shadowsocks.Controller | |||
namespace Shadowsocks.WPF.Behaviors | |||
{ | |||
static class HotkeyReg | |||
{ |
@@ -1,9 +1,9 @@ | |||
using System; | |||
using System; | |||
using System.IO.Pipes; | |||
using System.Net; | |||
using System.Text; | |||
namespace Shadowsocks.Controller | |||
namespace Shadowsocks.WPF.Behaviors | |||
{ | |||
class RequestAddUrlEventArgs : EventArgs | |||
{ | |||
@@ -11,7 +11,7 @@ namespace Shadowsocks.Controller | |||
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.ComponentModel; |
@@ -1,13 +1,11 @@ | |||
using System; | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
using System.Net; | |||
using System.Net.Http; | |||
using System.Threading.Tasks; | |||
using Newtonsoft.Json.Linq; | |||
using Shadowsocks.Model; | |||
using Shadowsocks.Models; | |||
namespace Shadowsocks.Controller.Service | |||
namespace Shadowsocks.WPF.Behaviors | |||
{ | |||
public class OnlineConfigResolver | |||
{ |
@@ -1,13 +1,8 @@ | |||
using Microsoft.Win32; | |||
using Microsoft.Win32; | |||
using NLog; | |||
using Shadowsocks.Util; | |||
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 | |||
{ |
@@ -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 WPFLocalizeExtension.Extensions; | |||
namespace Shadowsocks.WPF.Localization | |||
{ | |||
public class LocalizationProvider : ILocalizationProvider | |||
public class LocalizationProvider | |||
{ | |||
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] | |||
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.Linq; | |||
using System.Runtime.InteropServices; | |||
using System.Text; | |||
namespace Shadowsocks.Util.SystemProxy | |||
namespace Shadowsocks.WPF.Services.SystemProxy | |||
{ | |||
enum RasFieldSizeConst | |||
public enum RasFieldSizeConst | |||
{ | |||
MaxEntryName = 256, | |||
MaxPath = 260, | |||
} | |||
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] | |||
struct RasEntryName | |||
public struct RasEntryName | |||
{ | |||
public int dwSize; | |||
@@ -26,7 +25,8 @@ namespace Shadowsocks.Util.SystemProxy | |||
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = RAS.MaxPath + 1)] | |||
public string szPhonebookPath; | |||
} | |||
class RAS | |||
public class RAS | |||
{ | |||
public const int MaxEntryName = 256; | |||
public const int MaxPath = 260; |
@@ -1,11 +1,11 @@ | |||
using System; | |||
using System; | |||
using System.Collections.Generic; | |||
using System.ComponentModel; | |||
using System.Linq; | |||
using System.Runtime.InteropServices; | |||
using NLog; | |||
namespace Shadowsocks.Util.SystemProxy | |||
namespace Shadowsocks.WPF.Services.SystemProxy | |||
{ | |||
#region Windows API data structure definition | |||
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.Validation" Version="1.8.6" /> | |||
<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="ZXing.Net" Version="0.16.6" /> | |||
</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> | |||
<Compile Update="Localization\Strings.Designer.cs"> | |||
<DesignTime>True</DesignTime> | |||
@@ -58,4 +44,9 @@ | |||
<Folder Include="Resources\" /> | |||
</ItemGroup> | |||
<ItemGroup> | |||
<ProjectReference Include="..\Shadowsocks.Net\Shadowsocks.Net.csproj" /> | |||
<ProjectReference Include="..\Shadowsocks\Shadowsocks.csproj" /> | |||
</ItemGroup> | |||
</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 Shadowsocks.Controller.Service; | |||
using System; | |||
namespace Shadowsocks.WPF.ViewModels | |||
{ | |||
@@ -1,9 +1,9 @@ | |||
using ReactiveUI; | |||
using ReactiveUI; | |||
using ReactiveUI.Fody.Helpers; | |||
using ReactiveUI.Validation.Extensions; | |||
using ReactiveUI.Validation.Helpers; | |||
using Shadowsocks.Controller; | |||
using Shadowsocks.Localization; | |||
using Shadowsocks.WPF.Localization; | |||
using Shadowsocks.Model; | |||
using Shadowsocks.View; | |||
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> |