This commit is contained in:
kamhung
2015-12-09 11:12:33 +08:00
parent a44a7de4eb
commit d36397a3b9
17 changed files with 2195 additions and 0 deletions

View File

@@ -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<Servlet> createServletClassFilter() {
return createClassFilter(null, null, SocksServlet.class, null, "servlets", "servlet");
}
@Override
protected void loadServlet(ClassFilter<? extends Servlet> servletFilter) throws Exception {
if (socksServer != null) loadSocksServlet(this.nodeConf.getAnyValue("servlets"), servletFilter);
}
protected void loadSocksServlet(final AnyValue conf, ClassFilter<? extends Servlet> filter) throws Exception {
final StringBuilder sb = logger.isLoggable(Level.FINE) ? new StringBuilder() : null;
final String threadName = "[" + Thread.currentThread().getName() + "] ";
for (FilterEntry<? extends Servlet> en : filter.getFilterEntrys()) {
Class<SocksServlet> clazz = (Class<SocksServlet>) 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());
}
}

View File

@@ -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);
}
}

View File

@@ -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<SocksRequest, SocksResponse> {
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);
}
}
}

View File

@@ -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<Integer, Void>() {
@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<Integer, Void>() {
@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<Integer, Void> {
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<Integer, Void>() {
@Override
public void completed(Integer result, Void attachment) {
rbuffer.flip();
CompletionHandler parent = this;
response.sendBody(rbuffer.duplicate().asReadOnlyBuffer(), null, new CompletionHandler<Integer, Void>() {
@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<Integer, Void>() {
@Override
public void completed(Integer result, Void attachment) {
qbuffer.flip();
CompletionHandler parent = this;
remote.write(qbuffer, null, new CompletionHandler<Integer, Void>() {
@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) {
}
}
}
}

View File

@@ -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;
}
}

View File

@@ -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<SocksRequest> {
protected SocksResponse(Context context, SocksRequest request) {
super(context, request, (String[][]) null, (String[][]) null, null);
}
public static ObjectPool<Response> createPool(AtomicLong creatCounter, AtomicLong cycleCounter, int max, Creator<Response> creator) {
return new ObjectPool<>(creatCounter, cycleCounter, max, creator, (x) -> ((SocksResponse) x).prepare(), (x) -> ((SocksResponse) x).recycle());
}
@Override
public AsyncConnection removeChannel() {
return super.removeChannel();
}
}

View File

@@ -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<Integer, Void>() {
@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<Integer, Void>() {
@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<Integer, Void>() {
@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<Integer, Void>() {
@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<Integer, Void> {
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<Integer, Void>() {
@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);
}
}
}

View File

@@ -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<ByteBuffer> 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<Response> 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;
}
}

View File

@@ -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<SocksRequest, SocksResponse> {
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();
}
}

View File

@@ -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);
}
}

View File

@@ -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<String, Object> 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> T getAttribute(String name) {
return (T) attributes.get(name);
}
public Map<String, Object> getAttributes() {
return attributes;
}
public void setAttributes(Map<String, Object> 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;
}
}

View File

@@ -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));
}
}

View File

@@ -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<String, String> 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<String> getMPWxunionidByCode(String appid, String code) {
try {
Map<String, String> 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<String, String> 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<String, String> jsonmap = convert.convertFrom(TYPE_MAP_STRING_STRING, json);
return getMPUserTokenByOpenid(jsonmap.get("access_token"), jsonmap.get("openid"));
}
public Map<String, String> 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<String, String> 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算法生成安全签名
* <p>
* @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);
}
}
}

View File

@@ -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<String> {
//待支付
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;
}
}

View File

@@ -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("<([^/>]+)>(.+)</.+>"); // "<([^/>]+)><!\\[CDATA\\[(.+)\\]\\]></.+>"
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;
/**
* <xml><return_code><![CDATA[SUCCESS]]></return_code>
* + "<return_msg><![CDATA[OK]]></return_msg>
* + "<appid><![CDATA[wx4ad12c89818dd981]]></appid>
* + "<mch_id><![CDATA[1241384602]]></mch_id>
* + "<nonce_str><![CDATA[RpGucJ6wKtPgpTJy]]></nonce_str>
* + "<sign><![CDATA[DFD99D5DA7DCA4FB5FB79ECAD49B9369]]></sign>
* + "<result_code><![CDATA[SUCCESS]]></result_code>
* + "<prepay_id><![CDATA[wx2015051518135700aaea6bc30284682518]]></prepay_id>
* + "<trade_type><![CDATA[JSAPI]]></trade_type>
* + "</xml>
* <p>
* @param orderid
* @param payid
* @param orderpayid
* @param paymoney
* @param clientAddr
* @param notifyurl
* @param map
* @return
*/
public RetResult<Map<String, String>> paying(long orderid, long payid, long orderpayid, long paymoney, String clientAddr, String notifyurl, Map<String, String> 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<String, String> 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<String, String> 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<String, String> 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<String, String> 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<String, String> 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<String, String> 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;
}
}
/**
* <xml>
* <appid><![CDATA[wx4ad12c89818dd981]]></appid>
* <attach><![CDATA[10000070334]]></attach>
* <bank_type><![CDATA[ICBC_DEBIT]]></bank_type>
* <cash_fee><![CDATA[10]]></cash_fee>
* <fee_type><![CDATA[CNY]]></fee_type>
* <is_subscribe><![CDATA[Y]]></is_subscribe>
* <mch_id><![CDATA[1241384602]]></mch_id>
* <nonce_str><![CDATA[14d69ac6d6525f27dc9bcbebc]]></nonce_str>
* <openid><![CDATA[ojEVbsyDUzGqlgX3eDgmAMaUDucA]]></openid>
* <out_trade_no><![CDATA[1000072334]]></out_trade_no>
* <result_code><![CDATA[SUCCESS]]></result_code>
* <return_code><![CDATA[SUCCESS]]></return_code>
* <sign><![CDATA[60D95E25EA9C4F54BD1020952303C4E2]]></sign>
* <time_end><![CDATA[20150519085546]]></time_end>
* <total_fee>10</total_fee>
* <trade_type><![CDATA[JSAPI]]></trade_type>
* <transaction_id><![CDATA[1009630061201505190139511926]]></transaction_id>
* </xml>
* <p>
* @param map
* @return
*/
public WeiXinPayResult callbackPay(Map<String, String> 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<String, String> map) {
final StringBuilder sb = new StringBuilder();
sb.append("<xml>");
map.forEach((x, y) -> sb.append('<').append(x).append('>').append(y.replace("<", "&lt;").replace(">", "&gt;").replace("&", "&amp;")).append("</").append(x).append('>'));
sb.append("</xml>");
return sb.toString();
}
protected boolean checkSign(Map<String, String> 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<String, String> formatXMLToMap(final String xml) {
Map<String, String> map = new TreeMap<>();
Matcher m = PAYXML.matcher(xml.substring(xml.indexOf('>') + 1));
while (m.find()) {
String val = m.group(2);
if (val.startsWith("<![CDATA[")) val = val.substring("<![CDATA[".length(), val.length() - 3);
map.put(m.group(1), val);
}
return map;
}
}

View File

@@ -0,0 +1,117 @@
/*
* 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.convert.json.JsonFactory;
import java.util.*;
import java.util.function.*;
/**
* 微信企业号Service
*
* @author zhangjx
*/
public class WeiXinQYMessage {
private String agentid = "1";
private String msgtype = "text";
private Map<String, String> text;
private String touser = "@all";
private String toparty;
private String totag;
private String safe;
private Supplier<String> contentSupplier;
public WeiXinQYMessage() {
}
public WeiXinQYMessage(String agentid, String text) {
this.agentid = agentid;
setTextMessage(text);
}
public WeiXinQYMessage(String agentid, Supplier<String> 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<String, String> getText() {
return text;
}
public void setText(Map<String, String> 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);
}
}

View File

@@ -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<Map<String, String>>() {
}.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<String, String> 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<String> 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<String, String> 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;
}
/**
* 将公众平台回复用户的消息加密打包.
* <ol>
* <li>对要发送的消息进行AES-CBC加密</li>
* <li>生成安全签名</li>
* <li>将消息密文和安全签名打包成xml格式</li>
* </ol>
* <p>
* @param replyMsg 公众平台待回复用户的消息xml格式的字符串
* @param timeStamp 时间戳可以自己生成也可以用URL参数的timestamp
* @param nonce 随机串可以自己生成也可以用URL参数的nonce
* <p>
* @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 "<xml>\n<Encrypt><![CDATA[" + encrypt + "]]></Encrypt>\n"
+ "<MsgSignature><![CDATA[" + signature + "]]></MsgSignature>\n"
+ "<TimeStamp>" + timeStamp + "</TimeStamp>\n"
+ "<Nonce><![CDATA[" + nonce + "]]></Nonce>\n</xml>";
}
protected String decryptQYMessage(String msgSignature, String timeStamp, String nonce, String postData) {
// 密钥公众账号的app secret
// 提取密文
String encrypt = postData.substring(postData.indexOf("<Encrypt><![CDATA[") + "<Encrypt><![CDATA[".length(), postData.indexOf("]]></Encrypt>"));
// 验证安全签名
if (!sha1(qytoken, timeStamp, nonce, encrypt).equals(msgSignature)) throw new RuntimeException("signature verification error");
return decryptQY(encrypt);
}
/**
* 对明文进行加密.
* <p>
* @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算法生成安全签名
* <p>
* @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);
}
}
/**
* 获得对明文进行补位填充的字节.
* <p>
* @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);
}
/**
* 删除解密后明文的补位字符
* <p>
* @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);
}
}