From d36397a3b9b469a9c80e0082fc51328c6a4e332e Mon Sep 17 00:00:00 2001 From: kamhung <22250530@qq.com> Date: Wed, 9 Dec 2015 11:12:33 +0800 Subject: [PATCH] --- .../redkale/net/socks/NodeSocksServer.java | 71 ++++ .../net/socks/SocksConnectServlet.java | 58 ++++ .../net/socks/SocksPrepareServlet.java | 46 +++ .../redkale/net/socks/SocksProxyServlet.java | 198 +++++++++++ .../org/redkale/net/socks/SocksRequest.java | 88 +++++ .../org/redkale/net/socks/SocksResponse.java | 35 ++ .../org/redkale/net/socks/SocksRunner.java | 224 ++++++++++++ .../org/redkale/net/socks/SocksServer.java | 62 ++++ .../org/redkale/net/socks/SocksServlet.java | 28 ++ .../org/redkale/service/apns/ApnsMessage.java | 93 +++++ .../org/redkale/service/apns/ApnsPayload.java | 247 ++++++++++++++ .../org/redkale/service/apns/ApnsService.java | 165 +++++++++ .../service/weixin/WeiXinMPService.java | 106 ++++++ .../service/weixin/WeiXinPayResult.java | 77 +++++ .../service/weixin/WeiXinPayService.java | 259 ++++++++++++++ .../service/weixin/WeiXinQYMessage.java | 117 +++++++ .../service/weixin/WeiXinQYService.java | 321 ++++++++++++++++++ 17 files changed, 2195 insertions(+) create mode 100644 src-plugin/org/redkale/net/socks/NodeSocksServer.java create mode 100644 src-plugin/org/redkale/net/socks/SocksConnectServlet.java create mode 100644 src-plugin/org/redkale/net/socks/SocksPrepareServlet.java create mode 100644 src-plugin/org/redkale/net/socks/SocksProxyServlet.java create mode 100644 src-plugin/org/redkale/net/socks/SocksRequest.java create mode 100644 src-plugin/org/redkale/net/socks/SocksResponse.java create mode 100644 src-plugin/org/redkale/net/socks/SocksRunner.java create mode 100644 src-plugin/org/redkale/net/socks/SocksServer.java create mode 100644 src-plugin/org/redkale/net/socks/SocksServlet.java create mode 100644 src-plugin/org/redkale/service/apns/ApnsMessage.java create mode 100644 src-plugin/org/redkale/service/apns/ApnsPayload.java create mode 100644 src-plugin/org/redkale/service/apns/ApnsService.java create mode 100644 src-plugin/org/redkale/service/weixin/WeiXinMPService.java create mode 100644 src-plugin/org/redkale/service/weixin/WeiXinPayResult.java create mode 100644 src-plugin/org/redkale/service/weixin/WeiXinPayService.java create mode 100644 src-plugin/org/redkale/service/weixin/WeiXinQYMessage.java create mode 100644 src-plugin/org/redkale/service/weixin/WeiXinQYService.java diff --git a/src-plugin/org/redkale/net/socks/NodeSocksServer.java b/src-plugin/org/redkale/net/socks/NodeSocksServer.java new file mode 100644 index 000000000..9f0138430 --- /dev/null +++ b/src-plugin/org/redkale/net/socks/NodeSocksServer.java @@ -0,0 +1,71 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package org.redkale.net.socks; + +import org.redkale.net.Server; +import org.redkale.util.AnyValue; +import org.redkale.boot.Application; +import org.redkale.net.Servlet; +import org.redkale.boot.ClassFilter; +import org.redkale.boot.NodeServer; +import org.redkale.boot.NodeProtocol; +import org.redkale.boot.ClassFilter.FilterEntry; +import static org.redkale.boot.NodeServer.LINE_SEPARATOR; +import org.redkale.util.AnyValue.DefaultAnyValue; +import java.lang.reflect.*; +import java.net.*; +import java.util.logging.*; + +/** + * < server protocol="SOCKS" host="0.0.0.0" port="1080" bindaddr="外网IP"> < /server> + * + * @author zhangjx + */ +@NodeProtocol({"SOCKS"}) +public class NodeSocksServer extends NodeServer { + + private final SocksServer socksServer; + + public NodeSocksServer(Application application, AnyValue serconf) { + super(application, application.getResourceFactory().createChild(), createServer(application, serconf)); + this.socksServer = (SocksServer) server; + } + + private static Server createServer(Application application, AnyValue serconf) { + return new SocksServer(application.getStartTime(), application.getWatchFactory()); + } + + @Override + public InetSocketAddress getSocketAddress() { + return socksServer == null ? null : socksServer.getSocketAddress(); + } + + @Override + protected ClassFilter createServletClassFilter() { + return createClassFilter(null, null, SocksServlet.class, null, "servlets", "servlet"); + } + + @Override + protected void loadServlet(ClassFilter servletFilter) throws Exception { + if (socksServer != null) loadSocksServlet(this.nodeConf.getAnyValue("servlets"), servletFilter); + } + + protected void loadSocksServlet(final AnyValue conf, ClassFilter filter) throws Exception { + final StringBuilder sb = logger.isLoggable(Level.FINE) ? new StringBuilder() : null; + final String threadName = "[" + Thread.currentThread().getName() + "] "; + for (FilterEntry en : filter.getFilterEntrys()) { + Class clazz = (Class) en.getType(); + if (Modifier.isAbstract(clazz.getModifiers())) continue; + final SocksServlet servlet = clazz.newInstance(); + factory.inject(servlet); + DefaultAnyValue servletConf = (DefaultAnyValue) en.getProperty(); + this.socksServer.addSocksServlet(servlet, servletConf); + if (sb != null) sb.append(threadName).append(" Loaded ").append(clazz.getName()).append(" --> ").append(servletConf).append(LINE_SEPARATOR); + } + if (sb != null && sb.length() > 0) logger.log(Level.FINE, sb.toString()); + } + +} diff --git a/src-plugin/org/redkale/net/socks/SocksConnectServlet.java b/src-plugin/org/redkale/net/socks/SocksConnectServlet.java new file mode 100644 index 000000000..394ca11ed --- /dev/null +++ b/src-plugin/org/redkale/net/socks/SocksConnectServlet.java @@ -0,0 +1,58 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package org.redkale.net.socks; + +import org.redkale.util.AnyValue; +import org.redkale.net.Context; +import org.redkale.util.Utility; +import org.redkale.util.AutoLoad; +import java.io.*; +import java.net.*; +import java.nio.*; +import java.util.logging.*; + +/** + * + * @author zhangjx + */ +@AutoLoad(false) +public class SocksConnectServlet extends SocksServlet { + + private InetSocketAddress bindAddress; + + private byte[] bindAddressBytes = new byte[0]; + + @Override + public void init(Context context, AnyValue config) { + if (config == null) { + this.bindAddress = new InetSocketAddress(Utility.localInetAddress(), context.getServerAddress().getPort()); + } else { + this.bindAddress = new InetSocketAddress(config.getValue("bindaddr", Utility.localInetAddress().getHostAddress()), context.getServerAddress().getPort()); + } + Logger logger = context.getLogger(); + if (logger.isLoggable(Level.INFO)) logger.info("[" + Thread.currentThread().getName() + "] bindAddress = " + bindAddress); + ByteBuffer bb; + InetAddress addr = bindAddress.getAddress(); + if (addr instanceof Inet6Address) { + bb = ByteBuffer.allocate(1 + 16 + 2); + bb.put((byte) 0x04); + } else { + bb = ByteBuffer.allocate(1 + 4 + 2); + bb.put((byte) 0x01); + } + bb.put(addr.getAddress()); + bb.putChar((char) bindAddress.getPort()); + bb.flip(); + this.bindAddressBytes = bb.array(); + } + + @Override + public void execute(SocksRequest request, SocksResponse response) throws IOException { + response.getContext().submit(new SocksRunner(response.getContext(), response.removeChannel(), bindAddressBytes)); + response.finish(true); + } + +} diff --git a/src-plugin/org/redkale/net/socks/SocksPrepareServlet.java b/src-plugin/org/redkale/net/socks/SocksPrepareServlet.java new file mode 100644 index 000000000..899f5a4ea --- /dev/null +++ b/src-plugin/org/redkale/net/socks/SocksPrepareServlet.java @@ -0,0 +1,46 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package org.redkale.net.socks; + +import org.redkale.util.AnyValue; +import org.redkale.net.Context; +import org.redkale.net.PrepareServlet; +import java.io.*; + +/** + * + * @author zhangjx + */ +public final class SocksPrepareServlet extends PrepareServlet { + + private SocksServlet socksServlet = new SocksConnectServlet(); + + private SocksProxyServlet proxyServlet = new SocksProxyServlet(); + + public SocksPrepareServlet() { + } + + @Override + public void init(Context context, AnyValue config) { + if (socksServlet != null) socksServlet.init(context, socksServlet.conf == null ? config : socksServlet.conf); + } + + public void setSocksServlet(SocksServlet servlet, AnyValue conf) { + servlet.conf = conf; + if (servlet != null) this.socksServlet = servlet; + } + + // + @Override + public void execute(SocksRequest request, SocksResponse response) throws IOException { + if (request.isHttp()) { + proxyServlet.execute(request, response); + } else { + socksServlet.execute(request, response); + } + } + +} diff --git a/src-plugin/org/redkale/net/socks/SocksProxyServlet.java b/src-plugin/org/redkale/net/socks/SocksProxyServlet.java new file mode 100644 index 000000000..252416ed6 --- /dev/null +++ b/src-plugin/org/redkale/net/socks/SocksProxyServlet.java @@ -0,0 +1,198 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package org.redkale.net.socks; + +import org.redkale.net.AsyncConnection; +import org.redkale.util.Utility; +import org.redkale.util.AutoLoad; +import java.io.*; +import java.net.*; +import java.nio.*; +import java.nio.channels.*; + +/** + * 正向代理 + * + * @author zhangjx + */ +@AutoLoad(false) +public final class SocksProxyServlet extends SocksServlet { + + protected static final byte[] LINE = new byte[]{'\r', '\n'}; + + @Override + public void execute(SocksRequest request, SocksResponse response) throws IOException { + response.skipHeader(); + if ("CONNECT".equalsIgnoreCase(request.getMethod())) { + connect(request, response); + return; + } + String url = request.getRequestURI(); + url = url.substring(url.indexOf("://") + 3); + url = url.substring(url.indexOf('/')); + final ByteBuffer buffer = response.getContext().pollBuffer(); + buffer.put((request.getMethod() + " " + url + " HTTP/1.1\r\n").getBytes()); + for (String header : request.getHeaderNames()) { + if (!header.startsWith("Proxy-")) { + buffer.put((header + ": " + request.getHeader(header) + "\r\n").getBytes()); + } + } + if (request.getHost() != null) { + buffer.put(("Host: " + request.getHost() + "\r\n").getBytes()); + } + if (request.getContentType() != null) { + buffer.put(("Content-Type: " + request.getContentType() + "\r\n").getBytes()); + } + if (request.getContentLength() > 0) { + buffer.put(("Content-Length: " + request.getContentLength() + "\r\n").getBytes()); + } + buffer.put(LINE); + buffer.flip(); + final AsyncConnection remote = AsyncConnection.create("TCP", request.getHostSocketAddress(), 6, 6); + remote.write(buffer, null, new CompletionHandler() { + + @Override + public void completed(Integer result, Void attachment) { + if (buffer.hasRemaining()) { + remote.write(buffer, attachment, this); + return; + } + response.getContext().offerBuffer(buffer); + new ProxyCompletionHandler(remote, request, response).completed(0, null); + } + + @Override + public void failed(Throwable exc, Void attachment) { + response.getContext().offerBuffer(buffer); + response.finish(true); + try { + remote.close(); + } catch (IOException ex) { + } + } + }); + } + + private void connect(SocksRequest request, SocksResponse response) throws IOException { + final InetSocketAddress remoteAddress = request.parseSocketAddress(); + final AsyncConnection remote = remoteAddress.getPort() == 443 + ? AsyncConnection.create(Utility.createDefaultSSLSocket(remoteAddress)) : AsyncConnection.create("TCP", remoteAddress, 6, 6); + final ByteBuffer buffer0 = response.getContext().pollBuffer(); + buffer0.put("HTTP/1.1 200 Connection established\r\nConnection: close\r\n\r\n".getBytes()); + buffer0.flip(); + response.sendBody(buffer0, null, new CompletionHandler() { + + @Override + public void completed(Integer result, Void attachment) { + new ProxyCompletionHandler(remote, request, response).completed(0, null); + } + + @Override + public void failed(Throwable exc, Void attachment) { + response.finish(true); + try { + remote.close(); + } catch (IOException ex) { + } + } + }); + + } + + private static class ProxyCompletionHandler implements CompletionHandler { + + private AsyncConnection remote; + + private SocksRequest request; + + private SocksResponse response; + + public ProxyCompletionHandler(AsyncConnection remote, SocksRequest request, SocksResponse response) { + this.remote = remote; + this.request = request; + this.response = response; + } + + @Override + public void completed(Integer result0, Void v0) { + final ByteBuffer rbuffer = request.getContext().pollBuffer(); + remote.read(rbuffer, null, new CompletionHandler() { + + @Override + public void completed(Integer result, Void attachment) { + rbuffer.flip(); + CompletionHandler parent = this; + response.sendBody(rbuffer.duplicate().asReadOnlyBuffer(), null, new CompletionHandler() { + + @Override + public void completed(Integer result, Void attachment) { + rbuffer.clear(); + remote.read(rbuffer, attachment, parent); + } + + @Override + public void failed(Throwable exc, Void attachment) { + parent.failed(exc, attachment); + } + }); + } + + @Override + public void failed(Throwable exc, Void attachment) { + response.getContext().offerBuffer(rbuffer); + response.finish(true); + try { + remote.close(); + } catch (IOException ex) { + } + } + }); + + final ByteBuffer qbuffer = request.getContext().pollBuffer(); + request.getChannel().read(qbuffer, null, new CompletionHandler() { + + @Override + public void completed(Integer result, Void attachment) { + qbuffer.flip(); + CompletionHandler parent = this; + remote.write(qbuffer, null, new CompletionHandler() { + + @Override + public void completed(Integer result, Void attachment) { + qbuffer.clear(); + request.getChannel().read(qbuffer, null, parent); + } + + @Override + public void failed(Throwable exc, Void attachment) { + parent.failed(exc, attachment); + } + }); + } + + @Override + public void failed(Throwable exc, Void attachment) { + response.getContext().offerBuffer(qbuffer); + response.finish(true); + try { + remote.close(); + } catch (IOException ex) { + } + } + }); + } + + @Override + public void failed(Throwable exc, Void v) { + response.finish(true); + try { + remote.close(); + } catch (IOException ex) { + } + } + } + +} diff --git a/src-plugin/org/redkale/net/socks/SocksRequest.java b/src-plugin/org/redkale/net/socks/SocksRequest.java new file mode 100644 index 000000000..180c75861 --- /dev/null +++ b/src-plugin/org/redkale/net/socks/SocksRequest.java @@ -0,0 +1,88 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package org.redkale.net.socks; + +import org.redkale.net.AsyncConnection; +import org.redkale.net.http.HttpContext; +import org.redkale.net.http.HttpRequest; +import java.net.*; +import java.nio.*; + +/** + * + * @author zhangjx + */ +public class SocksRequest extends HttpRequest { + + private boolean http; + + private short requestid; + + protected SocksRequest(HttpContext context) { + super(context, null); + } + + @Override + protected int readHeader(ByteBuffer buffer) { + if (buffer.get(0) > 0x05 && buffer.remaining() > 3) { + this.http = true; + return super.readHeader(buffer); + } + this.http = false; + if (buffer.get() != 0x05) return -1; + if (buffer.get() != 0x01) return -1; + if (buffer.get() != 0x00) return -1; + return 0; + } + + protected InetSocketAddress parseSocketAddress() { + return HttpRequest.parseSocketAddress(getRequestURI()); + } + + @Override + protected InetSocketAddress getHostSocketAddress() { + return super.getHostSocketAddress(); + } + + @Override + protected AsyncConnection getChannel() { + return super.getChannel(); + } + + @Override + protected int readBody(ByteBuffer buffer) { + return buffer.remaining(); + } + + @Override + protected void prepare() { + super.prepare(); + } + + @Override + protected void recycle() { + this.requestid = 0; + this.http = false; + super.recycle(); + } + + public short getRequestid() { + return requestid; + } + + public void setRequestid(short requestid) { + this.requestid = requestid; + } + + public boolean isHttp() { + return http; + } + + public void setHttp(boolean http) { + this.http = http; + } + +} diff --git a/src-plugin/org/redkale/net/socks/SocksResponse.java b/src-plugin/org/redkale/net/socks/SocksResponse.java new file mode 100644 index 000000000..027502aae --- /dev/null +++ b/src-plugin/org/redkale/net/socks/SocksResponse.java @@ -0,0 +1,35 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package org.redkale.net.socks; + +import org.redkale.net.AsyncConnection; +import org.redkale.util.ObjectPool; +import org.redkale.net.Context; +import org.redkale.net.http.HttpResponse; +import org.redkale.util.Creator; +import org.redkale.net.Response; +import java.util.concurrent.atomic.*; + +/** + * + * @author zhangjx + */ +public class SocksResponse extends HttpResponse { + + protected SocksResponse(Context context, SocksRequest request) { + super(context, request, (String[][]) null, (String[][]) null, null); + } + + public static ObjectPool createPool(AtomicLong creatCounter, AtomicLong cycleCounter, int max, Creator creator) { + return new ObjectPool<>(creatCounter, cycleCounter, max, creator, (x) -> ((SocksResponse) x).prepare(), (x) -> ((SocksResponse) x).recycle()); + } + + @Override + public AsyncConnection removeChannel() { + return super.removeChannel(); + } + +} diff --git a/src-plugin/org/redkale/net/socks/SocksRunner.java b/src-plugin/org/redkale/net/socks/SocksRunner.java new file mode 100644 index 000000000..040baa2e1 --- /dev/null +++ b/src-plugin/org/redkale/net/socks/SocksRunner.java @@ -0,0 +1,224 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package org.redkale.net.socks; + +import org.redkale.net.AsyncConnection; +import org.redkale.net.Context; +import java.net.*; +import java.nio.*; +import java.nio.channels.*; +import java.util.logging.*; + +/** + * + * @author zhangjx + */ +public class SocksRunner implements Runnable { + + private final AsyncConnection channel; + + private final Logger logger; + + private final boolean finest; + + private final Context context; + + private final byte[] bindAddressBytes; + + private ByteBuffer buffer; + + protected boolean closed = false; + + private InetSocketAddress remoteAddress; + + private AsyncConnection remoteChannel; + + public SocksRunner(Context context, AsyncConnection channel, final byte[] bindAddressBytes) { + this.context = context; + this.logger = context.getLogger(); + this.finest = this.context.getLogger().isLoggable(Level.FINEST); + this.channel = channel; + this.buffer = context.pollBuffer(); + this.bindAddressBytes = bindAddressBytes; + } + + @Override + public void run() { + try { + ask(); + } catch (Exception e) { + closeRunner(e); + } + } + + private void ask() { + buffer.putChar((char) 0x0500); + buffer.flip(); + this.channel.write(buffer, null, new CompletionHandler() { + + @Override + public void completed(Integer result, Void attachment) { + if (buffer.hasRemaining()) { + channel.write(buffer, null, this); + return; + } + try { + connect(); + } catch (Exception e) { + closeRunner(e); + } + } + + @Override + public void failed(Throwable exc, Void attachment) { + closeRunner(exc); + } + }); + } + + private void connect() { + buffer.clear(); + this.channel.read(buffer, null, new CompletionHandler() { + + @Override + public void completed(Integer result, Void attachment) { + buffer.flip(); + if (buffer.getChar() != 0x0501) { + if (finest) logger.finest("connect header not 0x0501"); + closeRunner(null); + return; + } + char addrtype = buffer.getChar(); //0x0001 - 4 ; 0x0003 - x ; 0x0004 - 16 + try { + byte[] bytes = new byte[(addrtype == 0x0003) ? (buffer.get() & 0xff) : (addrtype * 4)]; + buffer.get(bytes); + remoteAddress = new InetSocketAddress((addrtype == 0x0003) ? InetAddress.getByName(new String(bytes)) : InetAddress.getByAddress(bytes), buffer.getChar()); + } catch (UnknownHostException e) { + failed(e, attachment); + return; + } + try { + remoteChannel = AsyncConnection.create("TCP", remoteAddress, 6, 6); + buffer.clear(); + buffer.putChar((char) 0x0500); + buffer.put((byte) 0x00); //rsv + buffer.put(bindAddressBytes); + buffer.flip(); + channel.write(buffer, null, new CompletionHandler() { + + @Override + public void completed(Integer result, Void attachment) { + if (buffer.hasRemaining()) { + channel.write(buffer, null, this); + return; + } + stream(); + } + + @Override + public void failed(Throwable exc, Void attachment) { + closeRunner(exc); + } + }); + } catch (Exception e) { + buffer.clear(); + buffer.putChar((char) 0x0504); + if (finest) logger.log(Level.FINEST, remoteAddress + " remote connect error", e); + channel.write(buffer, null, new CompletionHandler() { + + @Override + public void completed(Integer result, Void attachment) { + if (buffer.hasRemaining()) { + channel.write(buffer, null, this); + return; + } + closeRunner(null); + } + + @Override + public void failed(Throwable exc, Void attachment) { + closeRunner(exc); + } + }); + } + } + + @Override + public void failed(Throwable exc, Void attachment) { + closeRunner(exc); + } + }); + } + + private void stream() { + new StreamCompletionHandler(channel, remoteChannel).completed(0, null); + new StreamCompletionHandler(remoteChannel, channel).completed(0, null); + } + + public void closeRunner(final Throwable e) { + if (closed) return; + synchronized (this) { + if (closed) return; + closed = true; + try { + channel.close(); + } catch (Throwable t) { + } + context.offerBuffer(buffer); + buffer = null; + if (e != null && finest) { + logger.log(Level.FINEST, "close socks channel by error", e); + } + } + } + + private class StreamCompletionHandler implements CompletionHandler { + + private final AsyncConnection conn1; + + private final AsyncConnection conn2; + + private final ByteBuffer rbuffer; + + public StreamCompletionHandler(AsyncConnection conn1, AsyncConnection conn2) { + this.conn1 = conn1; + this.conn2 = conn2; + this.rbuffer = context.pollBuffer(); + this.rbuffer.flip(); + } + + @Override + public void completed(Integer result0, Void v0) { + final CompletionHandler self = this; + if (rbuffer.hasRemaining()) { + conn2.write(rbuffer, null, self); + return; + } + rbuffer.clear(); + conn1.read(rbuffer, null, new CompletionHandler() { + + @Override + public void completed(Integer result, Void attachment) { + rbuffer.flip(); + conn2.write(rbuffer, attachment, self); + } + + @Override + public void failed(Throwable exc, Void attachment) { + self.failed(exc, attachment); + } + }); + } + + @Override + public void failed(Throwable exc, Void v) { + context.offerBuffer(rbuffer); + conn1.dispose(); + conn2.dispose(); + if (finest) logger.log(Level.FINEST, "StreamCompletionHandler closed", exc); + } + } +} diff --git a/src-plugin/org/redkale/net/socks/SocksServer.java b/src-plugin/org/redkale/net/socks/SocksServer.java new file mode 100644 index 000000000..8df9aeb45 --- /dev/null +++ b/src-plugin/org/redkale/net/socks/SocksServer.java @@ -0,0 +1,62 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package org.redkale.net.socks; + +import org.redkale.util.AnyValue; +import org.redkale.net.Server; +import org.redkale.net.http.HttpContext; +import org.redkale.util.ObjectPool; +import org.redkale.net.Context; +import org.redkale.watch.WatchFactory; +import org.redkale.net.Response; +import java.nio.*; +import java.util.concurrent.atomic.*; + +/** + * + * @author zhangjx + */ +public final class SocksServer extends Server { + + public SocksServer() { + this(System.currentTimeMillis(), null); + } + + public SocksServer(long serverStartTime, final WatchFactory watch) { + super(serverStartTime, "TCP", new SocksPrepareServlet(), watch); + } + + @Override + public void init(AnyValue config) throws Exception { + super.init(config); + } + + public void addSocksServlet(SocksServlet servlet, AnyValue conf) { + ((SocksPrepareServlet) this.prepare).setSocksServlet(servlet, conf); + } + + @Override + @SuppressWarnings("unchecked") + protected Context createContext() { + final int port = this.address.getPort(); + AtomicLong createBufferCounter = watch == null ? new AtomicLong() : watch.createWatchNumber("SOCKS_" + port + ".Buffer.creatCounter"); + AtomicLong cycleBufferCounter = watch == null ? new AtomicLong() : watch.createWatchNumber("SOCKS_" + port + ".Buffer.cycleCounter"); + int rcapacity = Math.max(this.capacity, 8 * 1024); + ObjectPool bufferPool = new ObjectPool<>(createBufferCounter, cycleBufferCounter, this.bufferPoolSize, + (Object... params) -> ByteBuffer.allocateDirect(rcapacity), null, (e) -> { + if (e == null || e.isReadOnly() || e.capacity() != rcapacity) return false; + e.clear(); + return true; + }); + AtomicLong createResponseCounter = watch == null ? new AtomicLong() : watch.createWatchNumber("SOCKS_" + port + ".Response.creatCounter"); + AtomicLong cycleResponseCounter = watch == null ? new AtomicLong() : watch.createWatchNumber("SOCKS_" + port + ".Response.cycleCounter"); + ObjectPool responsePool = SocksResponse.createPool(createResponseCounter, cycleResponseCounter, this.responsePoolSize, null); + HttpContext localcontext = new HttpContext(this.serverStartTime, this.logger, executor, rcapacity, bufferPool, responsePool, + this.maxbody, this.charset, this.address, this.prepare, this.watch, this.readTimeoutSecond, this.writeTimeoutSecond, ""); + responsePool.setCreator((Object... params) -> new SocksResponse(localcontext, new SocksRequest(localcontext))); + return localcontext; + } +} diff --git a/src-plugin/org/redkale/net/socks/SocksServlet.java b/src-plugin/org/redkale/net/socks/SocksServlet.java new file mode 100644 index 000000000..d417a4d81 --- /dev/null +++ b/src-plugin/org/redkale/net/socks/SocksServlet.java @@ -0,0 +1,28 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package org.redkale.net.socks; + +import org.redkale.util.AnyValue; +import org.redkale.net.Servlet; + +/** + * + * @author zhangjx + */ +public abstract class SocksServlet implements Servlet { + + AnyValue conf; //当前Servlet的配置 + + @Override + public final boolean equals(Object obj) { + return obj != null && obj.getClass() == this.getClass(); + } + + @Override + public final int hashCode() { + return this.getClass().hashCode(); + } +} diff --git a/src-plugin/org/redkale/service/apns/ApnsMessage.java b/src-plugin/org/redkale/service/apns/ApnsMessage.java new file mode 100644 index 000000000..5eca111e3 --- /dev/null +++ b/src-plugin/org/redkale/service/apns/ApnsMessage.java @@ -0,0 +1,93 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package org.redkale.service.apns; + +import org.redkale.convert.json.JsonFactory; + +/** + * + * @author zhangjx + */ +public class ApnsMessage { + + public static final int PRIORITY_IMMEDIATELY = 10; + + public static final int PRIORITY_A_TIME = 5; + + private ApnsPayload payload; + + private int expiredate; + + private int priority = PRIORITY_IMMEDIATELY; + + private int identifier; + + private String token; + + public ApnsMessage() { + } + + public ApnsMessage(String token, ApnsPayload payload) { + this(token, payload, 0); + } + + public ApnsMessage(String token, ApnsPayload payload, int expiredate) { + this(token, payload, expiredate, PRIORITY_IMMEDIATELY); + } + + public ApnsMessage(String token, ApnsPayload payload, int expiredate, int priority) { + this.token = token; + this.payload = payload; + this.expiredate = expiredate; + this.priority = priority; + } + + public String getToken() { + return token; + } + + public void setToken(String token) { + this.token = token; + } + + public int getExpiredate() { + return expiredate; + } + + public void setExpiredate(int expiredate) { + this.expiredate = expiredate; + } + + public int getPriority() { + return priority; + } + + public void setPriority(int priority) { + this.priority = priority; + } + + public ApnsPayload getPayload() { + return payload; + } + + public void setPayload(ApnsPayload payload) { + this.payload = payload; + } + + public int getIdentifier() { + return identifier; + } + + public void setIdentifier(int identifier) { + this.identifier = identifier; + } + + @Override + public String toString() { + return JsonFactory.root().getConvert().convertTo(this); + } + +} diff --git a/src-plugin/org/redkale/service/apns/ApnsPayload.java b/src-plugin/org/redkale/service/apns/ApnsPayload.java new file mode 100644 index 000000000..fa210a08a --- /dev/null +++ b/src-plugin/org/redkale/service/apns/ApnsPayload.java @@ -0,0 +1,247 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package org.redkale.service.apns; + +import org.redkale.convert.json.JsonFactory; +import java.util.*; +import java.util.regex.*; + +/** + * + * @author zhangjx + */ +public class ApnsPayload { + + private static final Pattern regex = Pattern.compile("\""); + + //----------------------- alert --------------------------------- + private String alertTitle; + + private String alertBody; + + private String alertTitleLocKey; + + private String[] alertTitleLocArgs; + + private String alertActionLocKey; + + private String alertLocKey; + + private String[] alertLocArgs; + + private String alertLaunchImage; + + //-------------------------------------------------------- + private int contentAvailable; + + private String alert; + + private int badge; + + private String sound; + + private final Map attributes = new HashMap<>(); + + public ApnsPayload() { + } + + public ApnsPayload(String alert, int badge) { + this.alert = alert; + this.badge = badge; + } + + public ApnsPayload(String title, String body, int badge) { + this.alertTitle = title; + this.alertBody = body; + this.badge = badge; + } + + public void putAttribute(String name, Object value) { + attributes.put(name, value); + } + + public void removeAttribute(String name) { + attributes.remove(name); + } + + public T getAttribute(String name) { + return (T) attributes.get(name); + } + + public Map getAttributes() { + return attributes; + } + + public void setAttributes(Map map) { + if (map != null) attributes.putAll(map); + } + + @Override + public String toString() { + StringBuilder alertsb = new StringBuilder(); + if (alert != null) { + alertsb.append('"').append(regex.matcher(alert).replaceAll("\\\"")).append('"'); + } else { + alertsb.append('{'); + if (alertTitle != null) { + if (alertsb.length() > 1) alertsb.append(','); + alertsb.append("\"title\":\"").append(regex.matcher(alertTitle).replaceAll("\\\"")).append('"'); + } + if (alertBody != null) { + if (alertsb.length() > 1) alertsb.append(','); + alertsb.append("\"body\":\"").append(regex.matcher(alertBody).replaceAll("\\\"")).append('"'); + } + if (alertTitleLocKey != null) { + if (alertsb.length() > 1) alertsb.append(','); + alertsb.append("\"title-loc-key\":\"").append(regex.matcher(alertTitleLocKey).replaceAll("\\\"")).append('"'); + } + if (alertTitleLocArgs != null && alertTitleLocArgs.length > 0) { + if (alertsb.length() > 1) alertsb.append(','); + alertsb.append("\"title-loc-args\":["); + boolean first = true; + for (String str : alertTitleLocArgs) { + if (!first) alertsb.append(','); + alertsb.append('"').append(regex.matcher(str).replaceAll("\\\"")).append('"'); + first = false; + } + alertsb.append(']'); + } + if (alertActionLocKey != null) { + if (alertsb.length() > 1) alertsb.append(','); + alertsb.append("\"action-loc-key\":\"").append(regex.matcher(alertActionLocKey).replaceAll("\\\"")).append('"'); + } + if (alertLocKey != null) { + if (alertsb.length() > 1) alertsb.append(','); + alertsb.append("\"loc-key\":\"").append(regex.matcher(alertLocKey).replaceAll("\\\"")).append('"'); + } + if (alertLocArgs != null && alertLocArgs.length > 0) { + if (alertsb.length() > 1) alertsb.append(','); + alertsb.append("\"loc-args\":["); + boolean first = true; + for (String str : alertLocArgs) { + if (!first) alertsb.append(','); + alertsb.append('"').append(regex.matcher(str).replaceAll("\\\"")).append('"'); + first = false; + } + alertsb.append(']'); + } + if (alertLaunchImage != null) { + if (alertsb.length() > 1) alertsb.append(','); + alertsb.append("\"launch-image\":\"").append(regex.matcher(alertLaunchImage).replaceAll("\\\"")).append('"'); + } + alertsb.append('}'); + } + final StringBuilder sb = new StringBuilder(); + sb.append("{\"aps\":{\"alert\":").append(alertsb); + if (badge > 0) sb.append(",\"badge\":").append(badge); + if (contentAvailable > 0) sb.append(",\"content-available\":").append(contentAvailable); + if (sound != null) sb.append(",\"sound\":\"").append(sound).append('"'); + sb.append("}"); + if (attributes.isEmpty()) { + sb.append('}'); + } else { + sb.append(',').append(JsonFactory.root().getConvert().convertTo(attributes).substring(1)); + } + return sb.toString(); + } + + public String getAlertTitle() { + return alertTitle; + } + + public void setAlertTitle(String alertTitle) { + this.alertTitle = alertTitle; + } + + public String getAlertBody() { + return alertBody; + } + + public void setAlertBody(String alertBody) { + this.alertBody = alertBody; + } + + public String getAlertTitleLocKey() { + return alertTitleLocKey; + } + + public void setAlertTitleLocKey(String alertTitleLocKey) { + this.alertTitleLocKey = alertTitleLocKey; + } + + public String[] getAlertTitleLocArgs() { + return alertTitleLocArgs; + } + + public void setAlertTitleLocArgs(String[] alertTitleLocArgs) { + this.alertTitleLocArgs = alertTitleLocArgs; + } + + public String getAlertActionLocKey() { + return alertActionLocKey; + } + + public void setAlertActionLocKey(String alertActionLocKey) { + this.alertActionLocKey = alertActionLocKey; + } + + public String getAlertLocKey() { + return alertLocKey; + } + + public void setAlertLocKey(String alertLocKey) { + this.alertLocKey = alertLocKey; + } + + public String[] getAlertLocArgs() { + return alertLocArgs; + } + + public void setAlertLocArgs(String[] alertLocArgs) { + this.alertLocArgs = alertLocArgs; + } + + public String getAlertLaunchImage() { + return alertLaunchImage; + } + + public void setAlertLaunchImage(String alertLaunchImage) { + this.alertLaunchImage = alertLaunchImage; + } + + public int getContentAvailable() { + return contentAvailable; + } + + public void setContentAvailable(int contentAvailable) { + this.contentAvailable = contentAvailable; + } + + public String getAlert() { + return alert; + } + + public void setAlert(String alert) { + this.alert = alert; + } + + public int getBadge() { + return badge; + } + + public void setBadge(int badge) { + this.badge = badge; + } + + public String getSound() { + return sound; + } + + public void setSound(String sound) { + this.sound = sound; + } + +} diff --git a/src-plugin/org/redkale/service/apns/ApnsService.java b/src-plugin/org/redkale/service/apns/ApnsService.java new file mode 100644 index 000000000..501b67ecf --- /dev/null +++ b/src-plugin/org/redkale/service/apns/ApnsService.java @@ -0,0 +1,165 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package org.redkale.service.apns; + +import org.redkale.util.AnyValue; +import org.redkale.util.Utility; +import org.redkale.convert.json.JsonConvert; +import org.redkale.util.AutoLoad; +import org.redkale.service.LocalService; +import org.redkale.service.Service; +import org.redkale.convert.json.JsonFactory; +import java.io.*; +import java.net.*; +import java.nio.*; +import java.nio.charset.*; +import java.security.*; +import java.util.concurrent.*; +import java.util.logging.*; +import javax.annotation.*; +import javax.net.ssl.*; + +/** + * + * @author zhangjx + */ +@AutoLoad(false) +@LocalService +public class ApnsService implements Service { + + private static final Charset UTF8 = Charset.forName("UTF-8"); + + protected final Logger logger = Logger.getLogger(this.getClass().getSimpleName()); + + @Resource + protected JsonConvert convert; + + @Resource(name = "property.apns.certpwd") + protected String apnscertpwd = "1"; //证书的密码 + + @Resource(name = "property.apns.certpath") //用来加载证书用 + protected String apnscertpath = "apnspushdev_cert.p12"; + + @Resource(name = "property.apns.pushaddr") // + protected String apnspushaddr = "gateway.sandbox.push.apple.com"; + + @Resource(name = "property.apns.pushport") // + protected int apnspushport = 2195; + + @Resource(name = "property.apns.buffersize") // + protected int apnsbuffersize = 4096; + + private boolean inited = false; + + private final CountDownLatch cdl = new CountDownLatch(1); + + private SSLSocketFactory sslFactory; + + @Override + public void init(AnyValue conf) { + new Thread() { + { + setDaemon(true); + setPriority(Thread.MAX_PRIORITY); + } + + @Override + public void run() { + try { + final String path = "/" + this.getClass().getPackage().getName().replace('.', '/') + "/" + apnscertpath; + KeyStore ks = KeyStore.getInstance("PKCS12"); + InputStream in = ApnsService.class.getResourceAsStream(path); + KeyManagerFactory kf = null; + if (in != null) { + ks.load(in, apnscertpwd.toCharArray()); + in.close(); + kf = KeyManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + kf.init(ks, apnscertpwd.toCharArray()); + } + + TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + tmf.init((KeyStore) null); + SSLContext context = SSLContext.getInstance("TLS"); + context.init(kf == null ? new KeyManager[0] : kf.getKeyManagers(), tmf.getTrustManagers(), null); + ApnsService.this.sslFactory = context.getSocketFactory(); + } catch (Exception e) { + logger.log(Level.SEVERE, this.getClass().getSimpleName() + " init SSLContext error", e); + } finally { + inited = true; + cdl.countDown(); + } + } + }.start(); + } + + @Override + public void destroy(AnyValue conf) { + } + + private Socket getPushSocket() throws IOException { + if (!this.inited) { + try { + cdl.await(); + } catch (InterruptedException e) { + } + } + Socket pushSocket = sslFactory.createSocket(apnspushaddr, apnspushport); + pushSocket.setTcpNoDelay(true); + return pushSocket; + } + + public void pushApnsMessage(ApnsMessage message) throws IOException { + final byte[] tokens = Utility.hexToBin(message.getToken().replaceAll("\\s+", "")); + ByteBuffer buffer = ByteBuffer.allocate(apnsbuffersize); + buffer.put((byte) 2); //固定命令号 + buffer.putInt(0); //下面数据的长度 + + buffer.put((byte) 1); //token + buffer.putShort((short) tokens.length); + buffer.put(tokens); + + buffer.put((byte) 2); //payload + final byte[] payload = message.getPayload().toString().getBytes(UTF8); + buffer.putShort((short) payload.length); + buffer.put(payload); + + if (message.getIdentifier() > 0) { + buffer.put((byte) 3); //Notification identifier + buffer.putShort((short) 4); + buffer.putInt(message.getIdentifier()); + } + if (message.getExpiredate() > 0) { + buffer.put((byte) 4); //Expiration date + buffer.putShort((short) 4); + buffer.putInt(message.getExpiredate()); + } + buffer.put((byte) 5); //Priority + buffer.putShort((short) 1); + buffer.put((byte) message.getPriority()); + + final int pos = buffer.position(); + buffer.position(1); + buffer.putInt(pos - 5); + buffer.position(pos); + buffer.flip(); + + Socket socket = getPushSocket(); + socket.getOutputStream().write(buffer.array(), 0, buffer.remaining()); + socket.close(); + } + + public static void main(String[] args) throws Exception { + ApnsService service = new ApnsService(); + service.convert = JsonFactory.root().getConvert(); + service.init(null); + + final String token = "01727b19 b9f8abf4 0891e31d 3446479d a43902e1 819edc44 a073d951 b8b7db90"; + ApnsPayload payload = new ApnsPayload("您有新的消息", "这是消息内容", 1); + System.out.println(payload); + service.pushApnsMessage(new ApnsMessage(token, payload)); + } + +} diff --git a/src-plugin/org/redkale/service/weixin/WeiXinMPService.java b/src-plugin/org/redkale/service/weixin/WeiXinMPService.java new file mode 100644 index 000000000..3f8352f13 --- /dev/null +++ b/src-plugin/org/redkale/service/weixin/WeiXinMPService.java @@ -0,0 +1,106 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package org.redkale.service.weixin; + +import org.redkale.util.Utility; +import org.redkale.convert.json.JsonConvert; +import org.redkale.service.RetResult; +import org.redkale.util.AutoLoad; +import org.redkale.service.LocalService; +import org.redkale.service.Service; +import static org.redkale.convert.json.JsonConvert.TYPE_MAP_STRING_STRING; +import static org.redkale.util.Utility.getHttpContent; +import java.io.*; +import java.security.*; +import java.util.*; +import java.util.logging.*; +import javax.annotation.*; + +/** + * 微信服务号Service + * + * @author zhangjx + */ +@AutoLoad(false) +@LocalService +public class WeiXinMPService implements Service { + + protected final Logger logger = Logger.getLogger(this.getClass().getSimpleName()); + + private final boolean finest = logger.isLoggable(Level.FINEST); + + private final boolean finer = logger.isLoggable(Level.FINER); + + protected final Map mpsecrets = new HashMap<>(); + + @Resource + protected JsonConvert convert; + + // http://m.xxx.com/pipes/wx/verifymp + @Resource(name = "property.wxmp.token") + protected String mptoken = ""; + + @Resource(name = "property.wxmp.corpid") + protected String mpcorpid = "wxYYYYYYYYYYYYYY"; + + @Resource(name = "property.wxmp.aeskey") + protected String mpaeskey = ""; + + public WeiXinMPService() { + // mpsecrets.put("wxYYYYYYYYYYYYYYYYYY", "xxxxxxxxxxxxxxxxxxxxxxxxxxx"); + } + + //-----------------------------------微信服务号接口---------------------------------------------------------- + public RetResult getMPWxunionidByCode(String appid, String code) { + try { + Map wxmap = getMPUserTokenByCode(appid, code); + final String unionid = wxmap.get("unionid"); + if (unionid != null && !unionid.isEmpty()) return new RetResult<>(unionid); + return new RetResult<>(1011002); + } catch (IOException e) { + return new RetResult<>(1011001); + } + } + + public Map getMPUserTokenByCode(String appid, String code) throws IOException { + String url = "https://api.weixin.qq.com/sns/oauth2/access_token?appid=" + appid + "&secret=" + mpsecrets.get(appid) + "&code=" + code + "&grant_type=authorization_code"; + String json = getHttpContent(url); + if (finest) logger.finest(url + "--->" + json); + Map jsonmap = convert.convertFrom(TYPE_MAP_STRING_STRING, json); + return getMPUserTokenByOpenid(jsonmap.get("access_token"), jsonmap.get("openid")); + } + + public Map getMPUserTokenByOpenid(String access_token, String openid) throws IOException { + String url = "https://api.weixin.qq.com/sns/userinfo?access_token=" + access_token + "&openid=" + openid; + String json = getHttpContent(url); + if (finest) logger.finest(url + "--->" + json); + Map jsonmap = convert.convertFrom(TYPE_MAP_STRING_STRING, json.replaceFirst("\\[.*\\]", "null")); + return jsonmap; + } + + public String verifyMPURL(String msgSignature, String timeStamp, String nonce, String echoStr) { + String signature = sha1(mptoken, timeStamp, nonce); + if (!signature.equals(msgSignature)) throw new RuntimeException("signature verification error"); + return echoStr; + } + + /** + * 用SHA1算法生成安全签名 + *

+ * @param strings + * @return 安全签名 + */ + protected static String sha1(String... strings) { + try { + Arrays.sort(strings); + MessageDigest md = MessageDigest.getInstance("SHA-1"); + for (String s : strings) md.update(s.getBytes()); + return Utility.binToHexString(md.digest()); + } catch (Exception e) { + throw new RuntimeException("SHA encryption to generate signature failure", e); + } + } +} diff --git a/src-plugin/org/redkale/service/weixin/WeiXinPayResult.java b/src-plugin/org/redkale/service/weixin/WeiXinPayResult.java new file mode 100644 index 000000000..18064dc97 --- /dev/null +++ b/src-plugin/org/redkale/service/weixin/WeiXinPayResult.java @@ -0,0 +1,77 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package org.redkale.service.weixin; + +import org.redkale.service.RetResult; + +/** + * + * @author zhangjx + */ +public class WeiXinPayResult extends RetResult { + + //待支付 + public static final short PAYSTATUS_UNPAY = 10; + + //已支付 + public static final short PAYSTATUS_PAYOK = 30; + + private long orderid; + + private long payid; + + private long payedmoney; + + private short paystatus; + + public WeiXinPayResult() { + } + + public WeiXinPayResult(int retcode) { + super(retcode); + } + + public WeiXinPayResult(long orderid, long payid, short paystatus, long payedmoney, String resultcontent) { + this.orderid = orderid; + this.payid = payid; + this.paystatus = paystatus; + this.payedmoney = payedmoney; + this.setResult(resultcontent); + } + + public long getOrderid() { + return orderid; + } + + public void setOrderid(long orderid) { + this.orderid = orderid; + } + + public long getPayid() { + return payid; + } + + public void setPayid(long payid) { + this.payid = payid; + } + + public long getPayedmoney() { + return payedmoney; + } + + public void setPayedmoney(long payedmoney) { + this.payedmoney = payedmoney; + } + + public short getPaystatus() { + return paystatus; + } + + public void setPaystatus(short paystatus) { + this.paystatus = paystatus; + } + +} diff --git a/src-plugin/org/redkale/service/weixin/WeiXinPayService.java b/src-plugin/org/redkale/service/weixin/WeiXinPayService.java new file mode 100644 index 000000000..bfd9bbbab --- /dev/null +++ b/src-plugin/org/redkale/service/weixin/WeiXinPayService.java @@ -0,0 +1,259 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package org.redkale.service.weixin; + +import org.redkale.util.Utility; +import org.redkale.convert.json.JsonConvert; +import org.redkale.service.RetResult; +import org.redkale.util.AutoLoad; +import org.redkale.service.LocalService; +import org.redkale.service.Service; +import static org.redkale.service.weixin.WeiXinPayResult.*; +import java.security.*; +import java.text.*; +import java.util.*; +import java.util.logging.*; +import java.util.regex.*; +import javax.annotation.*; + +/** + * + * @author zhangjx + */ +@AutoLoad(false) +@LocalService +public class WeiXinPayService implements Service { + + private static final DateFormat FORMAT = new SimpleDateFormat("yyyyMMddHHmmss"); + + private static final Pattern PAYXML = Pattern.compile("<([^/>]+)>(.+)"); // "<([^/>]+)>" + + public static final int PAY_WX_ERROR = 4012101;//微信支付失败 + + public static final int PAY_FALSIFY_ORDER = 4012017;//交易签名被篡改 + + public static final int PAY_STATUS_ERROR = 4012018;//订单或者支付状态不正确 + + protected final Logger logger = Logger.getLogger(this.getClass().getSimpleName()); + + protected final boolean fine = logger.isLoggable(Level.FINE); + + protected final boolean finer = logger.isLoggable(Level.FINER); + + protected final boolean finest = logger.isLoggable(Level.FINEST); + + @Resource(name = "property.wxpay.appid") //公众账号ID + protected String wxpayappid = "wxYYYYYYYYYYYY"; + + @Resource(name = "property.wxpay.mchid") //商户ID + protected String wxpaymchid = "xxxxxxxxxxx"; + + @Resource(name = "property.wxpay.sdbmchid") //子商户ID,受理模式必填 + protected String wxpaysdbmchid = ""; + + @Resource(name = "property.wxpay.key") //签名算法需要用到的秘钥 + protected String wxpaykey = "##########################"; + + @Resource(name = "property.wxpay.certpwd") + protected String wxpaycertpwd = "xxxxxxxxxx"; //HTTP证书的密码,默认等于MCHID + + @Resource(name = "property.wxpay.certpath") //HTTP证书在服务器中的路径,用来加载证书用 + protected String wxpaycertpath = "apiclient_cert.p12"; + + @Resource + protected JsonConvert convert; + + /** + * + * + " + * + " + * + " + * + " + * + " + * + " + * + " + * + " + * + " + *

+ * @param orderid + * @param payid + * @param orderpayid + * @param paymoney + * @param clientAddr + * @param notifyurl + * @param map + * @return + */ + public RetResult> paying(long orderid, long payid, long orderpayid, long paymoney, String clientAddr, String notifyurl, Map map) { + RetResult result = null; + try { + if (!(map instanceof SortedMap)) map = new TreeMap<>(map); + map.put("appid", wxpayappid); + map.put("mch_id", wxpaymchid); + map.put("nonce_str", Long.toHexString(System.currentTimeMillis()) + Long.toHexString(System.nanoTime())); + map.putIfAbsent("body", "服务"); + map.put("attach", "" + payid); + map.put("out_trade_no", "" + orderpayid); + map.put("total_fee", "" + paymoney); + map.put("spbill_create_ip", clientAddr); + synchronized (FORMAT) { + map.put("time_expire", FORMAT.format(new Date(System.currentTimeMillis() + 10 * 60 * 60 * 1000))); + } + map.put("notify_url", notifyurl); + { + final StringBuilder sb = new StringBuilder(); + map.forEach((x, y) -> sb.append(x).append('=').append(y).append('&')); + sb.append("key=").append(wxpaykey); + map.put("sign", Utility.binToHexString(MessageDigest.getInstance("MD5").digest(sb.toString().getBytes())).toUpperCase()); + } + if (finest) logger.finest("weixinpaying2: " + orderid + " -> unifiedorder.map =" + map); + Map wxresult = formatXMLToMap(Utility.postHttpContent("https://api.mch.weixin.qq.com/pay/unifiedorder", formatMapToXML(map))); + if (finest) logger.finest("weixinpaying3: " + orderid + " -> unifiedorder.callback =" + wxresult); + if (!"SUCCESS".equals(wxresult.get("return_code"))) return new RetResult<>(PAY_WX_ERROR); + if (!checkSign(wxresult)) return new RetResult(PAY_FALSIFY_ORDER); + /** + * "appId" : "wx2421b1c4370ec43b", //公众号名称,由商户传入 "timeStamp":" 1395712654", //时间戳,自1970年以来的秒数 "nonceStr" : "e61463f8efa94090b1f366cccfbbb444", //随机串 "package" : + * "prepay_id=u802345jgfjsdfgsdg888", "signType" : "MD5", //微信签名方式: "paySign" : "70EA570631E4BB79628FBCA90534C63FF7FADD89" //微信签名 + */ + Map rs = new TreeMap<>(); + rs.put("appId", this.wxpayappid); + rs.put("timeStamp", Long.toString(System.currentTimeMillis() / 1000)); + rs.put("nonceStr", Long.toHexString(System.currentTimeMillis()) + Long.toHexString(System.nanoTime())); + rs.put("package", "prepay_id=" + wxresult.get("prepay_id")); + rs.put("signType", "MD5"); + { + final StringBuilder sb2 = new StringBuilder(); + rs.forEach((x, y) -> sb2.append(x).append('=').append(y).append('&')); + sb2.append("key=").append(wxpaykey); + rs.put("paySign", Utility.binToHexString(MessageDigest.getInstance("MD5").digest(sb2.toString().getBytes())).toUpperCase()); + } + if (finest) logger.finest("weixinpaying4: " + orderid + " -> unifiedorder.result =" + rs); + RetResult rr = new RetResult(rs); + rr.setRetinfo("" + orderpayid); + return rr; + } catch (Exception e) { + logger.log(Level.WARNING, "paying error.", e); + } + return result; + } + + public RetResult closepay(long orderpayid) { + RetResult result = null; + try { + Map map = new TreeMap<>(); + map.put("appid", wxpayappid); + map.put("mch_id", wxpaymchid); + map.put("nonce_str", Long.toHexString(System.currentTimeMillis()) + Long.toHexString(System.nanoTime())); + map.put("out_trade_no", "" + orderpayid); + { + final StringBuilder sb = new StringBuilder(); + map.forEach((x, y) -> sb.append(x).append('=').append(y).append('&')); + sb.append("key=").append(wxpaykey); + map.put("sign", Utility.binToHexString(MessageDigest.getInstance("MD5").digest(sb.toString().getBytes())).toUpperCase()); + } + if (finest) logger.finest("weixinclosepay2: " + orderpayid + " -> closeorder.map =" + map); + Map wxresult = formatXMLToMap(Utility.postHttpContent("https://api.mch.weixin.qq.com/pay/closeorder", formatMapToXML(map))); + if (finest) logger.finest("weixinclosepay3: " + orderpayid + " -> closeorder.callback =" + wxresult); + if (!"SUCCESS".equals(wxresult.get("return_code"))) return new RetResult<>(PAY_WX_ERROR); + if (!checkSign(wxresult)) return new RetResult(PAY_FALSIFY_ORDER); + return new RetResult(wxresult); + } catch (Exception e) { + logger.log(Level.WARNING, "closepay error: " + orderpayid, e); + } + return result; + } + + public WeiXinPayResult checkPay(long orderid, long orderpayid) { + WeiXinPayResult result = new WeiXinPayResult(PAY_STATUS_ERROR); + try { + Map map = new TreeMap<>(); + map.put("appid", wxpayappid); + map.put("mch_id", wxpaymchid); + map.put("out_trade_no", "" + orderpayid); + map.put("nonce_str", Long.toHexString(System.currentTimeMillis()) + Long.toHexString(System.nanoTime())); + { + final StringBuilder sb = new StringBuilder(); + map.forEach((x, y) -> sb.append(x).append('=').append(y).append('&')); + sb.append("key=").append(wxpaykey); + map.put("sign", Utility.binToHexString(MessageDigest.getInstance("MD5").digest(sb.toString().getBytes())).toUpperCase()); + } + Map wxresult = formatXMLToMap(Utility.postHttpContent("https://api.mch.weixin.qq.com/pay/orderquery", formatMapToXML(map))); + return callbackPay(wxresult); + } catch (Exception e) { + logger.log(Level.FINER, "check weixinpay[" + orderid + "] except", e); + return result; + } + } + + /** + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * 10 + * + * + * + *

+ * @param map + * @return + */ + public WeiXinPayResult callbackPay(Map map) { + if (!"SUCCESS".equals(map.get("return_code"))) return new WeiXinPayResult(PAY_WX_ERROR); + if (!(map instanceof SortedMap)) map = new TreeMap<>(map); + if (!checkSign(map)) return new WeiXinPayResult(PAY_FALSIFY_ORDER); + String state = map.get("trade_state"); + if (state == null && "SUCCESS".equals(map.get("result_code")) && Long.parseLong(map.get("total_fee")) > 0) { + state = "SUCCESS"; + } + short paystatus = "SUCCESS".equals(state) ? PAYSTATUS_PAYOK : PAYSTATUS_UNPAY; + return new WeiXinPayResult(Long.parseLong(map.get("out_trade_no")), Long.parseLong(map.get("attach")), paystatus, Long.parseLong(map.get("total_fee")), convert.convertTo(map)); + } + + protected static String formatMapToXML(final Map map) { + final StringBuilder sb = new StringBuilder(); + sb.append(""); + map.forEach((x, y) -> sb.append('<').append(x).append('>').append(y.replace("<", "<").replace(">", ">").replace("&", "&")).append("')); + sb.append(""); + return sb.toString(); + } + + protected boolean checkSign(Map map) { + if (!(map instanceof SortedMap)) map = new TreeMap<>(map); + String sign = map.remove("sign"); + final StringBuilder sb = new StringBuilder(); + map.forEach((x, y) -> sb.append(x).append('=').append(y).append('&')); + sb.append("key=").append(wxpaykey); + try { + return sign.equals(Utility.binToHexString(MessageDigest.getInstance("MD5").digest(sb.toString().getBytes())).toUpperCase()); + } catch (Exception e) { + return false; + } + } + + public static Map formatXMLToMap(final String xml) { + Map map = new TreeMap<>(); + Matcher m = PAYXML.matcher(xml.substring(xml.indexOf('>') + 1)); + while (m.find()) { + String val = m.group(2); + if (val.startsWith(" text; + + private String touser = "@all"; + + private String toparty; + + private String totag; + + private String safe; + + private Supplier contentSupplier; + + public WeiXinQYMessage() { + } + + public WeiXinQYMessage(String agentid, String text) { + this.agentid = agentid; + setTextMessage(text); + } + + public WeiXinQYMessage(String agentid, Supplier contentSupplier) { + this.agentid = agentid; + this.contentSupplier = contentSupplier; + } + + public final void setTextMessage(String content) { + if (text == null) text = new HashMap<>(); + text.put("content", content); + } + + public void supplyContent() { + if (contentSupplier != null) setTextMessage(contentSupplier.get()); + } + + public String getAgentid() { + return agentid; + } + + public void setAgentid(String agentid) { + this.agentid = agentid; + } + + public String getMsgtype() { + return msgtype; + } + + public void setMsgtype(String msgtype) { + this.msgtype = msgtype; + } + + public Map getText() { + return text; + } + + public void setText(Map text) { + this.text = text; + } + + public String getTouser() { + return touser; + } + + public void setTouser(String touser) { + this.touser = touser; + } + + public String getToparty() { + return toparty; + } + + public void setToparty(String toparty) { + this.toparty = toparty; + } + + public String getTotag() { + return totag; + } + + public void setTotag(String totag) { + this.totag = totag; + } + + public String getSafe() { + return safe; + } + + public void setSafe(String safe) { + this.safe = safe; + } + + @Override + public String toString() { + return JsonFactory.root().getConvert().convertTo(this); + } +} diff --git a/src-plugin/org/redkale/service/weixin/WeiXinQYService.java b/src-plugin/org/redkale/service/weixin/WeiXinQYService.java new file mode 100644 index 000000000..2a8c1b1f9 --- /dev/null +++ b/src-plugin/org/redkale/service/weixin/WeiXinQYService.java @@ -0,0 +1,321 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package org.redkale.service.weixin; + +import org.redkale.util.ByteArray; +import org.redkale.boot.Application; +import org.redkale.util.TypeToken; +import org.redkale.util.Utility; +import org.redkale.convert.json.JsonConvert; +import org.redkale.util.AutoLoad; +import org.redkale.service.LocalService; +import org.redkale.service.Service; +import org.redkale.net.WorkThread; +import static org.redkale.util.Utility.*; +import java.io.*; +import java.lang.reflect.*; +import java.nio.charset.*; +import java.security.*; +import java.util.*; +import java.util.function.*; +import java.util.logging.*; +import javax.annotation.*; +import javax.crypto.*; +import javax.crypto.spec.*; + +/** + * + * @author zhangjx + */ +@AutoLoad(false) +@LocalService +public class WeiXinQYService implements Service { + + protected final Logger logger = Logger.getLogger(this.getClass().getSimpleName()); + + private final boolean finest = logger.isLoggable(Level.FINEST); + + private final boolean finer = logger.isLoggable(Level.FINER); + + private static class Token { + + public String token; + + public long expires = 7100000; + + public long accesstime; + } + + private static final String BASE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + + private static final Charset CHARSET = Charset.forName("UTF-8"); + + private static final Random RANDOM = new Random(); + + protected static final Type MAPTYPE = new TypeToken>() { + }.getType(); + + @Resource + protected JsonConvert convert; + + //------------------------------------------------------------------------------------------------------ + // http://oa.xxxx.com/pipes/wx/verifyqy + @Resource(name = "property.wxqy.token") + protected String qytoken = ""; + + @Resource(name = "property.wxqy.corpid") + protected String qycorpid = "wxYYYYYYYYYYYYYYYY"; + + @Resource(name = "property.wxqy.aeskey") + protected String qyaeskey = ""; + + @Resource(name = "property.wxqy.secret") + private String qysecret = "#########################"; + + private SecretKeySpec qykeyspec; + + private IvParameterSpec qyivspec; + + private final Token qyAccessToken = new Token(); + + //------------------------------------------------------------------------------------------------------ + public WeiXinQYService() { + } + + public static void main(String[] args) throws Exception { + WeiXinQYService service = Application.singleton(WeiXinQYService.class); + + WeiXinQYMessage message = new WeiXinQYMessage(); + message.setTextMessage("【测试】duang!"); + message.setAgentid("2"); + service.sendQYMessage(message); + } + + //-----------------------------------微信企业号接口---------------------------------------------------------- + public Map getQYUserCode(String code, String agentid) throws IOException { + String url = "https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo?access_token=" + getQYAccessToken() + "&code=" + code + "&agentid=" + agentid; + String json = getHttpContent(url); + if (finest) logger.finest(url + "--->" + json); + return convert.convertFrom(MAPTYPE, json); + } + + public void sendQYTextMessage(String agentid, String message) { + sendQYMessage(new WeiXinQYMessage(agentid, message)); + } + + public void sendQYTextMessage(String agentid, Supplier contentSupplier) { + sendQYMessage(new WeiXinQYMessage(agentid, contentSupplier)); + } + + public void sendQYMessage(WeiXinQYMessage message) { + submit(() -> { + String result = null; + try { + message.supplyContent(); + String url = "https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=" + getQYAccessToken(); + result = postHttpContent(url, convert.convertTo(message)); + if (finest) logger.finest("sendQYMessage ok: " + message + " -> " + result); + } catch (Exception e) { + logger.log(Level.WARNING, "sendQYMessage error: " + message + " -> " + result, e); + } + }); + } + + public String verifyQYURL(String msgSignature, String timeStamp, String nonce, String echoStr) { + String signature = sha1(qytoken, timeStamp, nonce, echoStr); + if (!signature.equals(msgSignature)) throw new RuntimeException("signature verification error"); + return decryptQY(echoStr); + } + + protected String getQYAccessToken() throws IOException { + if (qyAccessToken.accesstime < System.currentTimeMillis() - qyAccessToken.expires) qyAccessToken.token = null; + if (qyAccessToken.token == null) { + String url = "https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=" + qycorpid + "&corpsecret=" + qysecret; + String json = getHttpContent(url); + if (finest) logger.finest(url + "--->" + json); + Map jsonmap = convert.convertFrom(MAPTYPE, json); + qyAccessToken.accesstime = System.currentTimeMillis(); + qyAccessToken.token = jsonmap.get("access_token"); + String exp = jsonmap.get("expires_in"); + if (exp != null) qyAccessToken.expires = (Integer.parseInt(exp) - 100) * 1000; + } + return qyAccessToken.token; + } + + /** + * 将公众平台回复用户的消息加密打包. + *

    + *
  1. 对要发送的消息进行AES-CBC加密
  2. + *
  3. 生成安全签名
  4. + *
  5. 将消息密文和安全签名打包成xml格式
  6. + *
+ *

+ * @param replyMsg 公众平台待回复用户的消息,xml格式的字符串 + * @param timeStamp 时间戳,可以自己生成,也可以用URL参数的timestamp + * @param nonce 随机串,可以自己生成,也可以用URL参数的nonce + *

+ * @return 加密后的可以直接回复用户的密文,包括msg_signature, timestamp, nonce, encrypt的xml格式的字符串 + */ + protected String encryptQYMessage(String replyMsg, String timeStamp, String nonce) { + // 加密 + String encrypt = encryptQY(random16String(), replyMsg); + + // 生成安全签名 + if (timeStamp == null || timeStamp.isEmpty()) timeStamp = Long.toString(System.currentTimeMillis()); + String signature = sha1(qytoken, timeStamp, nonce, encrypt); + + // System.out.println("发送给平台的签名是: " + signature[1].toString()); + // 生成发送的xml + return "\n\n" + + "\n" + + "" + timeStamp + "\n" + + "\n"; + } + + protected String decryptQYMessage(String msgSignature, String timeStamp, String nonce, String postData) { + // 密钥,公众账号的app secret + // 提取密文 + String encrypt = postData.substring(postData.indexOf("")); + // 验证安全签名 + if (!sha1(qytoken, timeStamp, nonce, encrypt).equals(msgSignature)) throw new RuntimeException("signature verification error"); + return decryptQY(encrypt); + } + + /** + * 对明文进行加密. + *

+ * @param randomStr + * @param text 需要加密的明文 + * @return 加密后base64编码的字符串 + */ + protected String encryptQY(String randomStr, String text) { + ByteArray bytes = new ByteArray(); + byte[] randomStrBytes = randomStr.getBytes(CHARSET); + byte[] textBytes = text.getBytes(CHARSET); + byte[] corpidBytes = qycorpid.getBytes(CHARSET); + + // randomStr + networkBytesOrder + text + qycorpid + bytes.add(randomStrBytes); + bytes.addInt(textBytes.length); + bytes.add(textBytes); + bytes.add(corpidBytes); + + // ... + pad: 使用自定义的填充方式对明文进行补位填充 + byte[] padBytes = encodePKCS7(bytes.count()); + bytes.add(padBytes); + + // 获得最终的字节流, 未加密 + try { + // 加密 + byte[] encrypted = createQYCipher(Cipher.ENCRYPT_MODE).doFinal(bytes.directBytes(), 0, bytes.count()); + // 使用BASE64对加密后的字符串进行编码 + return Base64.getEncoder().encodeToString(encrypted); + } catch (Exception e) { + throw new RuntimeException("AES加密失败", e); + } + } + + protected String decryptQY(String text) { + byte[] original; + try { + // 使用BASE64对密文进行解码 + original = createQYCipher(Cipher.DECRYPT_MODE).doFinal(Base64.getDecoder().decode(text)); + } catch (Exception e) { + throw new RuntimeException("AES解密失败", e); + } + try { + // 去除补位字符 + byte[] bytes = decodePKCS7(original); + // 分离16位随机字符串,网络字节序和corpid + int xmlLength = (bytes[16] & 0xFF) << 24 | (bytes[17] & 0xFF) << 16 | (bytes[18] & 0xFF) << 8 | bytes[19] & 0xFF; + if (!qycorpid.equals(new String(bytes, 20 + xmlLength, bytes.length - 20 - xmlLength, CHARSET))) { + throw new RuntimeException("corpid校验失败"); + } + return new String(bytes, 20, xmlLength, CHARSET); + } catch (RuntimeException e) { + if (e.getMessage().contains("corpid")) throw e; + throw new RuntimeException("解密后得到的buffer非法", e); + } + } + + protected Cipher createQYCipher(int mode) throws Exception { + Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding"); //AES192、256位加密解密 需要将新版 local_policy.jar、US_export_policy.jar两个文件覆盖到 ${JDK_HOME}/jre/lib/security/下 + if (qykeyspec == null) { + byte[] aeskeyBytes = Base64.getDecoder().decode(qyaeskey + "="); + qykeyspec = new SecretKeySpec(aeskeyBytes, "AES"); + qyivspec = new IvParameterSpec(aeskeyBytes, 0, 16); + } + cipher.init(mode, qykeyspec, qyivspec); + return cipher; + } + + protected void submit(Runnable runner) { + Thread thread = Thread.currentThread(); + if (thread instanceof WorkThread) { + ((WorkThread) thread).submit(runner); + return; + } + runner.run(); + } + + //-----------------------------------通用接口---------------------------------------------------------- + // 随机生成16位字符串 + protected static String random16String() { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < 16; i++) { + sb.append(BASE.charAt(RANDOM.nextInt(BASE.length()))); + } + return sb.toString(); + } + + /** + * 用SHA1算法生成安全签名 + *

+ * @param strings + * @return 安全签名 + */ + protected static String sha1(String... strings) { + try { + Arrays.sort(strings); + MessageDigest md = MessageDigest.getInstance("SHA-1"); + for (String s : strings) md.update(s.getBytes()); + return Utility.binToHexString(md.digest()); + } catch (Exception e) { + throw new RuntimeException("SHA encryption to generate signature failure", e); + } + } + + /** + * 获得对明文进行补位填充的字节. + *

+ * @param count 需要进行填充补位操作的明文字节个数 + * @return 补齐用的字节数组 + */ + private static byte[] encodePKCS7(int count) { + // 计算需要填充的位数 + int amountToPad = 32 - (count % 32); + if (amountToPad == 0) amountToPad = 32; + // 获得补位所用的字符 + char padChr = (char) (byte) (amountToPad & 0xFF); + StringBuilder tmp = new StringBuilder(); + for (int index = 0; index < amountToPad; index++) { + tmp.append(padChr); + } + return tmp.toString().getBytes(CHARSET); + } + + /** + * 删除解密后明文的补位字符 + *

+ * @param decrypted 解密后的明文 + * @return 删除补位字符后的明文 + */ + private static byte[] decodePKCS7(byte[] decrypted) { + int pad = (int) decrypted[decrypted.length - 1]; + if (pad < 1 || pad > 32) pad = 0; + return Arrays.copyOfRange(decrypted, 0, decrypted.length - pad); + } +}