cleaning structure

This commit is contained in:
Quality System Admin
2025-10-16 01:42:59 +03:00
parent e0ba349862
commit 50c791e242
469 changed files with 1016 additions and 29776 deletions

View File

@@ -0,0 +1,204 @@
/*
File: OSXAdapter.java
Abstract: Hooks existing preferences/about/quit functionality from an
existing Java app into handlers for the Mac OS X application menu.
Uses a Proxy object to dynamically implement the
com.apple.eawt.ApplicationListener interface and register it with the
com.apple.eawt.Application object. This allows the complete project
to be both built and run on any platform without any stubs or
placeholders. Useful for developers looking to implement Mac OS X
features while supporting multiple platforms with minimal impact.
Version: 2.0
Disclaimer: IMPORTANT: This Apple software is supplied to you by
Apple Inc. ("Apple") in consideration of your agreement to the
following terms, and your use, installation, modification or
redistribution of this Apple software constitutes acceptance of these
terms. If you do not agree with these terms, please do not use,
install, modify or redistribute this Apple software.
In consideration of your agreement to abide by the following terms, and
subject to these terms, Apple grants you a personal, non-exclusive
license, under Apple's copyrights in this original Apple software (the
"Apple Software"), to use, reproduce, modify and redistribute the Apple
Software, with or without modifications, in source and/or binary forms;
provided that if you redistribute the Apple Software in its entirety and
without modifications, you must retain this notice and the following
text and disclaimers in all such redistributions of the Apple Software.
Neither the name, trademarks, service marks or logos of Apple Inc.
may be used to endorse or promote products derived from the Apple
Software without specific prior written permission from Apple. Except
as expressly stated in this notice, no other rights or licenses, express
or implied, are granted by Apple herein, including but not limited to
any patent rights that may be infringed by your derivative works or by
other works in which the Apple Software may be incorporated.
The Apple Software is provided by Apple on an "AS IS" basis. APPLE
MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION
THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS
FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND
OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS.
IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL
OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION,
MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED
AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE),
STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
Copyright (C) 2003-2007 Apple, Inc., All Rights Reserved
*/
package com.apple;
import java.lang.reflect.*;
public class OSXAdapter implements InvocationHandler {
protected Object targetObject;
protected Method targetMethod;
protected String proxySignature;
static Object macOSXApplication;
// Pass this method an Object and Method equipped to perform application shutdown logic
// The method passed should return a boolean stating whether or not the quit should occur
public static void setQuitHandler(Object target, Method quitHandler) {
setHandler(new OSXAdapter("handleQuit", target, quitHandler));
}
// Pass this method an Object and Method equipped to display application info
// They will be called when the About menu item is selected from the application menu
public static void setAboutHandler(Object target, Method aboutHandler) {
boolean enableAboutMenu = (target != null && aboutHandler != null);
if (enableAboutMenu) {
setHandler(new OSXAdapter("handleAbout", target, aboutHandler));
}
// If we're setting a handler, enable the About menu item by calling
// com.apple.eawt.Application reflectively
try {
Method enableAboutMethod = macOSXApplication.getClass().getDeclaredMethod("setEnabledAboutMenu", new Class[] { boolean.class });
enableAboutMethod.invoke(macOSXApplication, new Object[] { Boolean.valueOf(enableAboutMenu) });
} catch (Exception ex) {
System.err.println("OSXAdapter could not access the About Menu");
ex.printStackTrace();
}
}
// Pass this method an Object and a Method equipped to display application options
// They will be called when the Preferences menu item is selected from the application menu
public static void setPreferencesHandler(Object target, Method prefsHandler) {
boolean enablePrefsMenu = (target != null && prefsHandler != null);
if (enablePrefsMenu) {
setHandler(new OSXAdapter("handlePreferences", target, prefsHandler));
}
// If we're setting a handler, enable the Preferences menu item by calling
// com.apple.eawt.Application reflectively
try {
Method enablePrefsMethod = macOSXApplication.getClass().getDeclaredMethod("setEnabledPreferencesMenu", new Class[] { boolean.class });
enablePrefsMethod.invoke(macOSXApplication, new Object[] { Boolean.valueOf(enablePrefsMenu) });
} catch (Exception ex) {
System.err.println("OSXAdapter could not access the About Menu");
ex.printStackTrace();
}
}
// Pass this method an Object and a Method equipped to handle document events from the Finder
// Documents are registered with the Finder via the CFBundleDocumentTypes dictionary in the
// application bundle's Info.plist
public static void setFileHandler(Object target, Method fileHandler) {
setHandler(new OSXAdapter("handleOpenFile", target, fileHandler) {
// Override OSXAdapter.callTarget to send information on the
// file to be opened
public boolean callTarget(Object appleEvent) {
if (appleEvent != null) {
try {
Method getFilenameMethod = appleEvent.getClass().getDeclaredMethod("getFilename", (Class[])null);
String filename = (String) getFilenameMethod.invoke(appleEvent, (Object[])null);
this.targetMethod.invoke(this.targetObject, new Object[] { filename });
} catch (Exception ex) {
}
}
return true;
}
});
}
// setHandler creates a Proxy object from the passed OSXAdapter and adds it as an ApplicationListener
public static void setHandler(OSXAdapter adapter) {
try {
Class<?> applicationClass = Class.forName("com.apple.eawt.Application");
if (macOSXApplication == null) {
macOSXApplication = applicationClass.getConstructor((Class[])null).newInstance((Object[])null);
}
Class<?> applicationListenerClass = Class.forName("com.apple.eawt.ApplicationListener");
Method addListenerMethod = applicationClass.getDeclaredMethod("addApplicationListener", new Class[] { applicationListenerClass });
// Create a proxy object around this handler that can be reflectively added as an Apple ApplicationListener
Object osxAdapterProxy = Proxy.newProxyInstance(OSXAdapter.class.getClassLoader(), new Class[] { applicationListenerClass }, adapter);
addListenerMethod.invoke(macOSXApplication, new Object[] { osxAdapterProxy });
} catch (ClassNotFoundException cnfe) {
System.err.println("This version of Mac OS X does not support the Apple EAWT. ApplicationEvent handling has been disabled (" + cnfe + ")");
} catch (Exception ex) { // Likely a NoSuchMethodException or an IllegalAccessException loading/invoking eawt.Application methods
System.err.println("Mac OS X Adapter could not talk to EAWT:");
ex.printStackTrace();
}
}
// Each OSXAdapter has the name of the EAWT method it intends to listen for (handleAbout, for example),
// the Object that will ultimately perform the task, and the Method to be called on that Object
protected OSXAdapter(String proxySignature, Object target, Method handler) {
this.proxySignature = proxySignature;
this.targetObject = target;
this.targetMethod = handler;
}
// Override this method to perform any operations on the event
// that comes with the various callbacks
// See setFileHandler above for an example
public boolean callTarget(Object appleEvent) throws InvocationTargetException, IllegalAccessException {
Object result = targetMethod.invoke(targetObject, (Object[])null);
if (result == null) {
return true;
}
return Boolean.valueOf(result.toString()).booleanValue();
}
// InvocationHandler implementation
// This is the entry point for our proxy object; it is called every time an ApplicationListener method is invoked
public Object invoke (Object proxy, Method method, Object[] args) throws Throwable {
if (isCorrectMethod(method, args)) {
boolean handled = callTarget(args[0]);
setApplicationEventHandled(args[0], handled);
}
// All of the ApplicationListener methods are void; return null regardless of what happens
return null;
}
// Compare the method that was called to the intended method when the OSXAdapter instance was created
// (e.g. handleAbout, handleQuit, handleOpenFile, etc.)
protected boolean isCorrectMethod(Method method, Object[] args) {
return (targetMethod != null && proxySignature.equals(method.getName()) && args.length == 1);
}
// It is important to mark the ApplicationEvent as handled and cancel the default behavior
// This method checks for a boolean result from the proxy method and sets the event accordingly
protected void setApplicationEventHandled(Object event, boolean handled) {
if (event != null) {
try {
Method setHandledMethod = event.getClass().getDeclaredMethod("setHandled", new Class[] { boolean.class });
// If the target method returns a boolean, use that as a hint
setHandledMethod.invoke(event, new Object[] { Boolean.valueOf(handled) });
} catch (Exception ex) {
System.err.println("OSXAdapter was unable to handle an ApplicationEvent: " + event);
ex.printStackTrace();
}
}
}
}

View File

@@ -0,0 +1,77 @@
/**
* @author Tres Finocchiaro
*
* Copyright (C) 2019 Tres Finocchiaro, QZ Industries, LLC
*
* LGPL 2.1 This is free software. This software and source code are released under
* the "LGPL 2.1 License". A copy of this license should be distributed with
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
*/
package com.apple;
import com.github.zafarkhaja.semver.Version;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import qz.common.Constants;
import qz.utils.MacUtilities;
import java.awt.*;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
/**
* Java 9+ compatible shim around Apple's OSXAdapter
*
* @author Tres Finocchiaro
*/
public class OSXAdapterWrapper implements InvocationHandler {
private static final Logger log = LogManager.getLogger(OSXAdapterWrapper.class);
public static final boolean legacyMode = Constants.JAVA_VERSION.lessThan(Version.valueOf("9.0.0"));
private Object target;
private Method handler;
public OSXAdapterWrapper(Object target, Method handler) {
this.target = target;
this.handler = handler;
}
public static void setQuitHandler(Object target, Method handler) {
if (!legacyMode) {
// Java 9+
wrap("java.awt.desktop.QuitHandler", "setQuitHandler", target, handler);
} else {
// Java 7, 8
OSXAdapter.setAboutHandler(target, handler);
}
}
public static void setAboutHandler(Object target, Method handler) {
if (!legacyMode) {
// Java 9+
wrap("java.awt.desktop.AboutHandler", "setAboutHandler", target, handler);
} else {
// Java 7, 8
OSXAdapter.setAboutHandler(target, handler);
}
}
public static void wrap(String className, String methodName, Object target, Method handler) {
try {
Class desktop = Desktop.getDesktop().getClass();
Class<?> handlerClass = Class.forName(className);
Method method = desktop.getDeclaredMethod(methodName, new Class<?>[] {handlerClass});
Object proxy = Proxy.newProxyInstance(MacUtilities.class.getClassLoader(), new Class<?>[] {handlerClass}, new OSXAdapterWrapper(target, handler));
method.invoke(Desktop.getDesktop(), new Object[] {proxy});
} catch (Exception e) {
log.warn("Failed to set {}", className, e.getMessage());
}
}
public Object invoke(Object proxy, Method m, Object[] args) throws Throwable {
handler.invoke(target);
return null;
}
}

View File

@@ -0,0 +1 @@
javax.usb.services=org.usb4java.javax.Services

17
old code/tray/src/log4j2.xml Executable file
View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<Configuration>
<Appenders>
<Console name="console" target="SYSTEM_OUT">
<PatternLayout pattern="[%p] %d{ISO8601} @ %c:%L%n\t%m%n"/>
</Console>
</Appenders>
<Loggers>
<!-- Packages -->
<Logger name="qz" level="trace"/>
<Logger name="qz.build.provision" level="info"/>
<!-- Default -->
<Root level="warn">
<AppenderRef ref="console"/>
</Root>
</Loggers>
</Configuration>

View File

@@ -0,0 +1,96 @@
/*
* The MIT License
*
* Copyright 2020 dyorgio.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.dyorgio.jna.platform.mac;
import com.sun.jna.Callback;
import com.sun.jna.NativeLong;
import com.sun.jna.Pointer;
import java.util.HashMap;
/**
*
* @author dyorgio
*/
@SuppressWarnings("Convert2Lambda")
public final class ActionCallback extends NSObject {
private static final NativeLong actionCallbackClass = Foundation.INSTANCE.objc_allocateClassPair(objectClass, ActionCallback.class.getSimpleName(), 0);
private static final Pointer actionCallbackSel = Foundation.INSTANCE.sel_registerName("actionCallback");
private static final Pointer setTargetSel = Foundation.INSTANCE.sel_registerName("setTarget:");
private static final Pointer setActionSel = Foundation.INSTANCE.sel_registerName("setAction:");
private static final Callback registerActionCallback;
static {
startNativeAppMainThread();
registerActionCallback = new Callback() {
@SuppressWarnings("unused")
public void callback(Pointer self, Pointer selector) {
if (selector.equals(actionCallbackSel)) {
ActionCallback action;
synchronized (callbackMap) {
action = callbackMap.get(Pointer.nativeValue(self));
}
if (action != null) {
action.runnable.run();
}
}
}
};
if (!Foundation.INSTANCE.class_addMethod(actionCallbackClass,
actionCallbackSel, registerActionCallback, "v@:")) {
throw new RuntimeException("Error initializing ActionCallback as a objective C class");
}
Foundation.INSTANCE.objc_registerClassPair(actionCallbackClass);
}
private static final HashMap<Long, ActionCallback> callbackMap = new HashMap<Long, ActionCallback>();
private final Runnable runnable;
@SuppressWarnings("LeakingThisInConstructor")
public ActionCallback(Runnable callable) {
super(Foundation.INSTANCE.class_createInstance(actionCallbackClass, 0));
this.runnable = callable;
synchronized (callbackMap) {
callbackMap.put(getId().longValue(), this);
}
}
@Override
public void release() {
synchronized (callbackMap) {
callbackMap.remove(getId().longValue());
}
super.release();
}
public void installActionOnNSControl(NativeLong nsControl) {
Foundation.INSTANCE.objc_msgSend(nsControl, setTargetSel, id);
Foundation.INSTANCE.objc_msgSend(nsControl, setActionSel, actionCallbackSel);
}
}

View File

@@ -0,0 +1,71 @@
/*
* The MIT License
*
* Copyright 2020 dyorgio.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.dyorgio.jna.platform.mac;
import com.sun.jna.Callback;
import com.sun.jna.Library;
import com.sun.jna.Native;
import com.sun.jna.NativeLong;
import com.sun.jna.Pointer;
/**
*
* @author dyorgio
*/
public interface Foundation extends Library {
Foundation INSTANCE = Native.load("Foundation", Foundation.class);
NativeLong class_getInstanceVariable(NativeLong classPointer, String name);
NativeLong object_getIvar(NativeLong target, NativeLong ivar);
NativeLong objc_getClass(String className);
NativeLong objc_allocateClassPair(NativeLong superClass, String name, long extraBytes);
void objc_registerClassPair(NativeLong clazz);
NativeLong class_createInstance(NativeLong clazz, int extraBytes);
boolean class_addMethod(NativeLong clazz, Pointer selector, Callback callback, String types);
NativeLong objc_msgSend(NativeLong receiver, Pointer selector);
NativeLong objc_msgSend(NativeLong receiver, Pointer selector, Pointer obj);
NativeLong objc_msgSend(NativeLong receiver, Pointer selector, NativeLong objAddress);
NativeLong objc_msgSend(NativeLong receiver, Pointer selector, boolean boolArg);
NativeLong objc_msgSend(NativeLong receiver, Pointer selector, double doubleArg);
// Used by NSObject.performSelectorOnMainThread
NativeLong objc_msgSend(NativeLong receiver, Pointer selector, Pointer selectorDst, NativeLong objAddress, boolean wait);
// Used by NSString.fromJavaString
NativeLong objc_msgSend(NativeLong receiver, Pointer selector, byte[] bytes, int len, long encoding);
Pointer sel_registerName(String selectorName);
}

View File

@@ -0,0 +1,104 @@
/*
* The MIT License
*
* Copyright 2020 dyorgio.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.dyorgio.jna.platform.mac;
import com.sun.jna.NativeLong;
import com.sun.jna.Pointer;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
*
* @author dyorgio
*/
public final class FoundationUtil {
private static final Foundation FOUNDATION = Foundation.INSTANCE;
public static final NativeLong NULL = new NativeLong(0l);
private FoundationUtil() {
}
public static boolean isNull(NativeLong id) {
return NULL.equals(id);
}
public static boolean isNull(NSObject object) {
return NULL.equals(object.id);
}
public static boolean isFalse(NativeLong id) {
return NULL.equals(id);
}
public static boolean isTrue(NativeLong id) {
return !NULL.equals(id);
}
public static NativeLong invoke(NativeLong id, String selector) {
return FOUNDATION.objc_msgSend(id, Foundation.INSTANCE.sel_registerName(selector));
}
public static NativeLong invoke(NativeLong id, String selector, boolean boolArg) {
return FOUNDATION.objc_msgSend(id, Foundation.INSTANCE.sel_registerName(selector), boolArg);
}
public static NativeLong invoke(NativeLong id, String selector, double doubleArg) {
return FOUNDATION.objc_msgSend(id, Foundation.INSTANCE.sel_registerName(selector), doubleArg);
}
public static NativeLong invoke(NativeLong id, String selector, NativeLong objAddress) {
return FOUNDATION.objc_msgSend(id, Foundation.INSTANCE.sel_registerName(selector), objAddress);
}
public static NativeLong invoke(NativeLong id, Pointer selectorPointer) {
return FOUNDATION.objc_msgSend(id, selectorPointer);
}
public static NativeLong invoke(NativeLong id, Pointer selectorPointer, NativeLong objAddress) {
return FOUNDATION.objc_msgSend(id, selectorPointer, objAddress);
}
public static void runOnMainThreadAndWait(Runnable runnable) throws InterruptedException, ExecutionException {
runOnMainThread(runnable, true);
}
public static FutureTask runOnMainThread(Runnable runnable, boolean waitUntilDone) {
FutureTask futureTask = new FutureTask(runnable, null);
FutureTaskCallback.performOnMainThread(futureTask, waitUntilDone);
return futureTask;
}
public static <T> T callOnMainThreadAndWait(Callable<T> callable) throws InterruptedException, ExecutionException {
return callOnMainThread(callable, true).get();
}
public static <T> FutureTask<T> callOnMainThread(Callable<T> callable, boolean waitUntilDone) {
FutureTask<T> futureTask = new FutureTask(callable);
FutureTaskCallback.performOnMainThread(futureTask, waitUntilDone);
return futureTask;
}
}

View File

@@ -0,0 +1,94 @@
/*
* The MIT License
*
* Copyright 2020 dyorgio.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.dyorgio.jna.platform.mac;
import com.sun.jna.Callback;
import com.sun.jna.NativeLong;
import com.sun.jna.Pointer;
import java.util.HashMap;
import java.util.concurrent.FutureTask;
/**
*
* @author dyorgio
*/
@SuppressWarnings("Convert2Lambda")
class FutureTaskCallback<T> extends NSObject {
private static final NativeLong futureTaskCallbackClass = Foundation.INSTANCE.objc_allocateClassPair(objectClass, FutureTaskCallback.class.getSimpleName(), 0);
private static final Pointer futureTaskCallbackSel = Foundation.INSTANCE.sel_registerName("futureTaskCallback");
private static final Callback registerFutureTaskCallback;
static {
startNativeAppMainThread();
registerFutureTaskCallback = new Callback() {
@SuppressWarnings("unused")
public void callback(Pointer self, Pointer selector) {
if (selector.equals(futureTaskCallbackSel)) {
FutureTaskCallback action;
synchronized (callbackMap) {
action = callbackMap.remove(Pointer.nativeValue(self));
}
if (action != null) {
action.callable.run();
}
}
}
};
if (!Foundation.INSTANCE.class_addMethod(futureTaskCallbackClass,
futureTaskCallbackSel, registerFutureTaskCallback, "v@:")) {
throw new RuntimeException("Error initializing FutureTaskCallback as a objective C class");
}
Foundation.INSTANCE.objc_registerClassPair(futureTaskCallbackClass);
}
private static final HashMap<Long, FutureTaskCallback> callbackMap = new HashMap<Long, FutureTaskCallback>();
private final FutureTask<T> callable;
@SuppressWarnings("LeakingThisInConstructor")
private FutureTaskCallback(FutureTask<T> callable) {
super(Foundation.INSTANCE.class_createInstance(futureTaskCallbackClass, 0));
this.callable = callable;
synchronized (callbackMap) {
callbackMap.put(getId().longValue(), this);
}
}
@Override
public void release() {
synchronized (callbackMap) {
callbackMap.remove(getId().longValue());
}
super.release();
}
static <T> void performOnMainThread(FutureTask<T> futureTask, boolean waitUntilDone) {
new FutureTaskCallback(futureTask).performSelectorOnMainThread(futureTaskCallbackSel, null, waitUntilDone);
}
}

View File

@@ -0,0 +1,22 @@
package org.dyorgio.jna.platform.mac;
import com.sun.jna.NativeLong;
import com.sun.jna.Pointer;
public class NSApplication extends NSObject {
static NativeLong klass = Objc.INSTANCE.objc_lookUpClass("NSApplication");
static Pointer sharedApplication = Objc.INSTANCE.sel_getUid("sharedApplication");
static Pointer activateIgnoringOtherApps = Objc.INSTANCE.sel_getUid("activateIgnoringOtherApps:");
public static NSApplication sharedApplication() {
return new NSApplication(Objc.INSTANCE.objc_msgSend(klass, sharedApplication));
}
public NSApplication(NativeLong handle) {
super(handle);
}
public void activateIgnoringOtherApps(boolean flag) {
Objc.INSTANCE.objc_msgSend(this.getId(), activateIgnoringOtherApps, flag);
}
}

View File

@@ -0,0 +1,50 @@
/*
* The MIT License
*
* Copyright 2020 dyorgio.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.dyorgio.jna.platform.mac;
import com.sun.jna.NativeLong;
import com.sun.jna.Pointer;
/**
*
* @author dyorgio
*/
public class NSDictionary extends NSObject {
private static final NativeLong dictionaryClass = Foundation.INSTANCE.objc_getClass("NSDictionary");
private static final Pointer dictionaryWithContentsOfFileSel = Foundation.INSTANCE.sel_registerName("dictionaryWithContentsOfFile:");
private static final Pointer objectForKeySel = Foundation.INSTANCE.sel_registerName("objectForKey:");
public NSDictionary(NativeLong id) {
super(id);
}
public static NSDictionary dictionaryWithContentsOfFile(NSString file) {
return new NSDictionary(Foundation.INSTANCE.objc_msgSend(dictionaryClass, dictionaryWithContentsOfFileSel, file.id));
}
public NSObject objectForKey(NSObject key) {
return new NSString(FoundationUtil.invoke(id, objectForKeySel, key.id));
}
}

View File

@@ -0,0 +1,96 @@
/*
* The MIT License
*
* Copyright 2020 dyorgio.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.dyorgio.jna.platform.mac;
import com.sun.jna.NativeLong;
import com.sun.jna.Pointer;
import java.awt.*;
import java.lang.reflect.InvocationTargetException;
import javax.swing.SwingUtilities;
/**
*
* @author dyorgio
*/
public class NSObject {
static final NativeLong objectClass = Foundation.INSTANCE.objc_getClass("NSObject");
protected static final Pointer allocSel = Foundation.INSTANCE.sel_registerName("alloc");
protected static final Pointer initSel = Foundation.INSTANCE.sel_registerName("init");
protected static final Pointer releaseSel = Foundation.INSTANCE.sel_registerName("release");
protected static final Pointer performSelectorOnMainThread$withObject$waitUntilDoneSel
= Foundation.INSTANCE.sel_registerName("performSelectorOnMainThread:withObject:waitUntilDone:");
final NativeLong id;
public NSObject(NativeLong id) {
this.id = id;
}
public final NativeLong getId() {
return id;
}
public void release() {
Foundation.INSTANCE.objc_msgSend(id, releaseSel);
}
@Override
@SuppressWarnings({"FinalizeDeclaration","deprecation","removal"})
protected void finalize() throws Throwable {
release();
super.finalize();
}
public void performSelectorOnMainThread(Pointer selector, NativeLong object, boolean waitUntilDone) {
Foundation.INSTANCE.objc_msgSend(id, //
NSObject.performSelectorOnMainThread$withObject$waitUntilDoneSel, //
selector, object, waitUntilDone);
}
static volatile boolean initialized = false;
static void startNativeAppMainThread() {
if (!initialized) {
synchronized (NSObject.objectClass) {
if (!initialized) {
try {
if(EventQueue.isDispatchThread()) {
Toolkit.getDefaultToolkit();
} else {
SwingUtilities.invokeAndWait(() -> Toolkit.getDefaultToolkit());
}
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
} catch (InvocationTargetException ex) {
// ignore
} finally {
initialized = true;
}
}
}
}
}
}

View File

@@ -0,0 +1,74 @@
/*
* The MIT License
*
* Copyright 2020 dyorgio.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.dyorgio.jna.platform.mac;
import com.sun.jna.NativeLong;
import com.sun.jna.Pointer;
import com.sun.jna.platform.mac.CoreFoundation;
import java.nio.charset.Charset;
/**
*
* @author dyorgio
*/
public class NSString extends NSObject {
public static final Charset UTF_16LE_CHARSET = Charset.forName("UTF-16LE");
private static final NativeLong stringCls = Foundation.INSTANCE.objc_getClass("NSString");
private static final Pointer stringSel = Foundation.INSTANCE.sel_registerName("string");
private static final Pointer initWithBytesLengthEncodingSel = Foundation.INSTANCE.sel_registerName("initWithBytes:length:encoding:");
private static final long NSUTF16LittleEndianStringEncoding = 0x94000100;
public NSString(String string) {
this(fromJavaString(string));
}
public NSString(NativeLong id) {
super(id);
}
@Override
public String toString() {
if (FoundationUtil.isNull(this)) {
return null;
}
CoreFoundation.CFStringRef cfString = new CoreFoundation.CFStringRef(new Pointer(id.longValue()));
try {
return CoreFoundation.INSTANCE.CFStringGetLength(cfString).intValue() > 0 ? cfString.stringValue() : "";
} finally {
cfString.release();
}
}
private static NativeLong fromJavaString(String s) {
if (s.isEmpty()) {
return Foundation.INSTANCE.objc_msgSend(stringCls, stringSel);
}
byte[] utf16Bytes = s.getBytes(UTF_16LE_CHARSET);
return Foundation.INSTANCE.objc_msgSend(Foundation.INSTANCE.objc_msgSend(stringCls, allocSel),
initWithBytesLengthEncodingSel, utf16Bytes, utf16Bytes.length, NSUTF16LittleEndianStringEncoding);
}
}

View File

@@ -0,0 +1,50 @@
/*
* The MIT License
*
* Copyright 2020 dyorgio.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.dyorgio.jna.platform.mac;
import com.sun.jna.NativeLong;
import com.sun.jna.Pointer;
/**
*
* @author dyorgio
*/
public class NSUserDefaults extends NSObject {
private static final NativeLong userDefaultsClass = Foundation.INSTANCE.objc_getClass("NSUserDefaults");
private static final Pointer standardUserDefaultsdSel = Foundation.INSTANCE.sel_registerName("standardUserDefaults");
private static final Pointer stringForKeySel = Foundation.INSTANCE.sel_registerName("stringForKey:");
public NSUserDefaults(NativeLong id) {
super(id);
}
public static NSUserDefaults standard() {
return new NSUserDefaults(Foundation.INSTANCE.objc_msgSend(userDefaultsClass, standardUserDefaultsdSel));
}
public NSString stringForKey(NSString key) {
return new NSString(FoundationUtil.invoke(id, stringForKeySel, key.id));
}
}

View File

@@ -0,0 +1,12 @@
package org.dyorgio.jna.platform.mac;
import com.sun.jna.Library;
import com.sun.jna.Native;
import com.sun.jna.NativeLong;
import com.sun.jna.Pointer;
public interface Objc extends Library {
Objc INSTANCE = Native.load("objc", Objc.class);
NativeLong objc_lookUpClass(String name);
Pointer sel_getUid(String str);
NativeLong objc_msgSend(NativeLong receiver, Pointer selector, Object... args);
}

View File

@@ -0,0 +1,190 @@
/*
* Copyright 2008 Sun Microsystems, Inc., 4150 Network Circle,
* Santa Clara, California 95054, U.S.A. All rights reserved.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
package org.jdesktop.swinghelper.tray;
import qz.common.Constants;
import qz.utils.MacUtilities;
import qz.utils.SystemUtilities;
import qz.utils.WindowsUtilities;
import javax.swing.*;
import javax.swing.event.PopupMenuListener;
import javax.swing.event.PopupMenuEvent;
import java.awt.*;
import java.awt.geom.Ellipse2D;
import java.awt.image.BufferedImage;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.ActionListener;
import java.awt.event.ActionEvent;
public class JXTrayIcon extends TrayIcon {
private JPopupMenu menu;
private static JDialog dialog;
static {
dialog = new JDialog((Frame) null);
dialog.setUndecorated(true);
dialog.setAlwaysOnTop(true);
}
private static PopupMenuListener popupListener = new PopupMenuListener() {
public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
}
public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
dialog.setVisible(false);
}
public void popupMenuCanceled(PopupMenuEvent e) {
dialog.setVisible(false);
}
};
public JXTrayIcon(Image image) {
super(image);
addMouseListener(new MouseAdapter() {
public void mouseReleased(MouseEvent e) {
showJPopupMenu(e);
}
});
}
protected void showJPopupMenu(MouseEvent mouseEvent) {
if (menu != null) {
Point location = mouseEvent.getLocationOnScreen();
// Handle HiDPI factor discrepancy between mouse position and window position
if(SystemUtilities.isWindows() && Constants.JAVA_VERSION.getMajorVersion() >= 9) {
location.setLocation(location.getX() / WindowsUtilities.getScaleFactor(), location.getY() / WindowsUtilities.getScaleFactor());
}
showJPopupMenu((int)location.getX(), (int)location.getY());
}
}
protected void showJPopupMenu(int x, int y) {
dialog.setLocation(x, y);
dialog.setVisible(true);
// Show the menu centered on the invisible dialog
menu.show(dialog.getContentPane(), 0, 0);
// Compensate position variance due to off-screen placement
Dimension menuSize = menu.getPreferredSize();
Point menuLocation = menu.getLocationOnScreen();
int x2 = x; int y2 = y;
if (menuLocation.getY() > y) y2 = y + menuSize.height;
if (menuLocation.getY() < y) y2 = y - menuSize.height;
if (menuLocation.getX() > x) x2 = x + menuSize.width;
if (menuLocation.getX() < x) x2 = x - menuSize.width;
if(x2 != x || y2 != y) {
menu.setLocation(x2, y2);
}
// popup works only for focused windows
dialog.toFront();
}
public JPopupMenu getJPopupMenu() {
return menu;
}
public void setJPopupMenu(JPopupMenu menu) {
if (this.menu != null) {
this.menu.removePopupMenuListener(popupListener);
}
this.menu = menu;
menu.addPopupMenuListener(popupListener);
}
private static void createGui() {
JXTrayIcon tray = new JXTrayIcon(createImage());
tray.setJPopupMenu(createJPopupMenu());
try {
SystemTray.getSystemTray().add(tray);
} catch (AWTException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws Exception {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
SwingUtilities.invokeLater(() -> createGui());
}
static Image createImage() {
BufferedImage i = new BufferedImage(32, 32, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2 = (Graphics2D) i.getGraphics();
g2.setColor(Color.RED);
g2.fill(new Ellipse2D.Float(0, 0, i.getWidth(), i.getHeight()));
g2.dispose();
return i;
}
static JPopupMenu createJPopupMenu() {
final JPopupMenu m = new JPopupMenu();
m.add(new JMenuItem("Item 1"));
m.add(new JMenuItem("Item 2"));
JMenu submenu = new JMenu("Submenu");
submenu.add(new JMenuItem("item 1"));
submenu.add(new JMenuItem("item 2"));
submenu.add(new JMenuItem("item 3"));
m.add(submenu);
JMenuItem exitItem = new JMenuItem("Exit");
exitItem.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
System.exit(0);
}
});
m.add(exitItem);
return m;
}
@Override
public Dimension getSize() {
Dimension iconSize = new Dimension(super.getSize());
switch(SystemUtilities.getOs()) {
// macOS icons are slightly smaller than the size reported
case MAC:
// Handle retina display
int macScale = MacUtilities.getScaleFactor();
// Handle undocumented icon border (e.g. 20px has 16px icon)
// See also IconCache.fixTrayIcons()
iconSize.width -= iconSize.width / 5;
iconSize.height -= iconSize.height / 5;
return new Dimension(iconSize.width * macScale, iconSize.height * macScale);
case WINDOWS:
if(Constants.JAVA_VERSION.getMajorVersion() >= 9) {
// JDK9+ required for HiDPI tray icons on Windows
double winScale = WindowsUtilities.getScaleFactor();
// Handle undocumented HiDPI icon support
// Requires TrayIcon.setImageAutoSize(true);
iconSize.width *= winScale;
iconSize.height *= winScale;
}
break;
}
return iconSize;
}
}

View File

@@ -0,0 +1,812 @@
/**
* Copyright (c) 2011-2013, Lukas Eder, lukas.eder@gmail.com
* All rights reserved.
*
* This software is licensed to you under the Apache License, Version 2.0
* (the "License"); You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* . Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* . Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* . Neither the name "jOOR" nor the names of its contributors may be
* used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
package org.joor;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Proxy;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;
/**
* A wrapper for an {@link Object} or {@link Class} upon which reflective calls
* can be made.
* <p>
* An example of using <code>Reflect</code> is <code><pre>
* // Static import all reflection methods to decrease verbosity
* import static org.joor.Reflect.*;
*
* // Wrap an Object / Class / class name with the on() method:
* on("java.lang.String")
* // Invoke constructors using the create() method:
* .create("Hello World")
* // Invoke methods using the call() method:
* .call("toString")
* // Retrieve the wrapped object
*
* @author Lukas Eder
* @author Irek Matysiewicz
*/
public class Reflect {
// ---------------------------------------------------------------------
// Static API used as entrance points to the fluent API
// ---------------------------------------------------------------------
/**
* Wrap a class name.
* <p>
* This is the same as calling <code>on(Class.forName(name))</code>
*
* @param name A fully qualified class name
* @return A wrapped class object, to be used for further reflection.
* @throws ReflectException If any reflection exception occurred.
* @see #on(Class)
*/
public static Reflect on(String name) throws ReflectException {
return on(forName(name));
}
/**
* Wrap a class name, loading it via a given class loader.
* <p>
* This is the same as calling
* <code>on(Class.forName(name, classLoader))</code>
*
* @param name A fully qualified class name.
* @param classLoader The class loader in whose context the class should be
* loaded.
* @return A wrapped class object, to be used for further reflection.
* @throws ReflectException If any reflection exception occurred.
* @see #on(Class)
*/
public static Reflect on(String name, ClassLoader classLoader) throws ReflectException {
return on(forName(name, classLoader));
}
/**
* Wrap a class.
* <p>
* Use this when you want to access static fields and methods on a
* {@link Class} object, or as a basis for constructing objects of that
* class using {@link #create(Object...)}
*
* @param clazz The class to be wrapped
* @return A wrapped class object, to be used for further reflection.
*/
public static Reflect on(Class<?> clazz) {
return new Reflect(clazz);
}
/**
* Wrap an object.
* <p>
* Use this when you want to access instance fields and methods on any
* {@link Object}
*
* @param object The object to be wrapped
* @return A wrapped object, to be used for further reflection.
*/
public static Reflect on(Object object) {
return new Reflect(object);
}
/**
* Conveniently render an {@link AccessibleObject} accessible.
* <p>
* To prevent {@link SecurityException}, this is only done if the argument
* object and its declaring class are non-public.
*
* @param accessible The object to render accessible
* @return The argument object rendered accessible
*/
public static <T extends AccessibleObject> T accessible(T accessible) {
if (accessible == null) {
return null;
}
if (accessible instanceof Member) {
Member member = (Member) accessible;
if (Modifier.isPublic(member.getModifiers()) &&
Modifier.isPublic(member.getDeclaringClass().getModifiers())) {
return accessible;
}
}
// [jOOQ #3392] The accessible flag is set to false by default, also for public members.
if (!accessible.isAccessible()) {
accessible.setAccessible(true);
}
return accessible;
}
// ---------------------------------------------------------------------
// Members
// ---------------------------------------------------------------------
/**
* The wrapped object
*/
private final Object object;
/**
* A flag indicating whether the wrapped object is a {@link Class} (for
* accessing static fields and methods), or any other type of {@link Object}
* (for accessing instance fields and methods).
*/
private final boolean isClass;
// ---------------------------------------------------------------------
// Constructors
// ---------------------------------------------------------------------
private Reflect(Class<?> type) {
this.object = type;
this.isClass = true;
}
private Reflect(Object object) {
this.object = object;
this.isClass = false;
}
// ---------------------------------------------------------------------
// Fluent Reflection API
// ---------------------------------------------------------------------
/**
* Get the wrapped object
*
* @param <T> A convenience generic parameter for automatic unsafe casting
*/
@SuppressWarnings("unchecked")
public <T> T get() {
return (T) object;
}
/**
* Set a field value.
* <p>
* This is roughly equivalent to {@link Field#set(Object, Object)}. If the
* wrapped object is a {@link Class}, then this will set a value to a static
* member field. If the wrapped object is any other {@link Object}, then
* this will set a value to an instance member field.
*
* @param name The field name
* @param value The new field value
* @return The same wrapped object, to be used for further reflection.
* @throws ReflectException If any reflection exception occurred.
*/
public Reflect set(String name, Object value) throws ReflectException {
try {
Field field = field0(name);
field.set(object, unwrap(value));
return this;
}
catch(Exception e) {
throw new ReflectException(e);
}
}
/**
* Get a field value.
* <p>
* This is roughly equivalent to {@link Field#get(Object)}. If the wrapped
* object is a {@link Class}, then this will get a value from a static
* member field. If the wrapped object is any other {@link Object}, then
* this will get a value from an instance member field.
* <p>
* If you want to "navigate" to a wrapped version of the field, use
* {@link #field(String)} instead.
*
* @param name The field name
* @return The field value
* @throws ReflectException If any reflection exception occurred.
* @see #field(String)
*/
public <T> T get(String name) throws ReflectException {
return field(name).<T>get();
}
/**
* Get a wrapped field.
* <p>
* This is roughly equivalent to {@link Field#get(Object)}. If the wrapped
* object is a {@link Class}, then this will wrap a static member field. If
* the wrapped object is any other {@link Object}, then this wrap an
* instance member field.
*
* @param name The field name
* @return The wrapped field
* @throws ReflectException If any reflection exception occurred.
*/
public Reflect field(String name) throws ReflectException {
try {
Field field = field0(name);
return on(field.get(object));
}
catch(Exception e) {
throw new ReflectException(e);
}
}
private Field field0(String name) throws ReflectException {
Class<?> type = type();
// Try getting a public field
try {
return type.getField(name);
}
// Try again, getting a non-public field
catch(NoSuchFieldException e) {
do {
try {
return accessible(type.getDeclaredField(name));
}
catch(NoSuchFieldException ignore) {}
type = type.getSuperclass();
}
while(type != null);
throw new ReflectException(e);
}
}
/**
* Get a Map containing field names and wrapped values for the fields'
* values.
* <p>
* If the wrapped object is a {@link Class}, then this will return static
* fields. If the wrapped object is any other {@link Object}, then this will
* return instance fields.
* <p>
* These two calls are equivalent <code><pre>
* on(object).field("myField");
* on(object).fields().get("myField");
* </pre></code>
*
* @return A map containing field names and wrapped values.
*/
public Map<String, Reflect> fields() {
Map<String, Reflect> result = new LinkedHashMap<String, Reflect>();
Class<?> type = type();
do {
for(Field field : type.getDeclaredFields()) {
if (!isClass ^ Modifier.isStatic(field.getModifiers())) {
String name = field.getName();
if (!result.containsKey(name)) { result.put(name, field(name)); }
}
}
type = type.getSuperclass();
}
while(type != null);
return result;
}
/**
* Call a method by its name.
* <p>
* This is a convenience method for calling
* <code>call(name, new Object[0])</code>
*
* @param name The method name
* @return The wrapped method result or the same wrapped object if the
* method returns <code>void</code>, to be used for further
* reflection.
* @throws ReflectException If any reflection exception occurred.
* @see #call(String, Object...)
*/
public Reflect call(String name) throws ReflectException {
return call(name, new Object[0]);
}
/**
* Call a method by its name.
* <p>
* This is roughly equivalent to {@link Method#invoke(Object, Object...)}.
* If the wrapped object is a {@link Class}, then this will invoke a static
* method. If the wrapped object is any other {@link Object}, then this will
* invoke an instance method.
* <p>
* Just like {@link Method#invoke(Object, Object...)}, this will try to wrap
* primitive types or unwrap primitive type wrappers if applicable. If
* several methods are applicable, by that rule, the first one encountered
* is called. i.e. when calling <code><pre>
* on(...).call("method", 1, 1);
* </pre></code> The first of the following methods will be called:
* <code><pre>
* public void method(int param1, Integer param2);
* public void method(Integer param1, int param2);
* public void method(Number param1, Number param2);
* public void method(Number param1, Object param2);
* public void method(int param1, Object param2);
* </pre></code>
* <p>
* The best matching method is searched for with the following strategy:
* <ol>
* <li>public method with exact signature match in class hierarchy</li>
* <li>non-public method with exact signature match on declaring class</li>
* <li>public method with similar signature in class hierarchy</li>
* <li>non-public method with similar signature on declaring class</li>
* </ol>
*
* @param name The method name
* @param args The method arguments
* @return The wrapped method result or the same wrapped object if the
* method returns <code>void</code>, to be used for further
* reflection.
* @throws ReflectException If any reflection exception occurred.
*/
public Reflect call(String name, Object... args) throws ReflectException {
Class<?>[] types = types(args);
// Try invoking the "canonical" method, i.e. the one with exact
// matching argument types
try {
Method method = exactMethod(name, types);
return on(method, object, args);
}
// If there is no exact match, try to find a method that has a "similar"
// signature if primitive argument types are converted to their wrappers
catch (NoSuchMethodException e) {
try {
Method method = similarMethod(name, types);
return on(method, object, args);
} catch (NoSuchMethodException e1) {
throw new ReflectException(e1);
}
}
}
/**
* Searches a method with the exact same signature as desired.
* <p>
* If a public method is found in the class hierarchy, this method is returned.
* Otherwise a private method with the exact same signature is returned.
* If no exact match could be found, we let the {@code NoSuchMethodException} pass through.
*/
private Method exactMethod(String name, Class<?>[] types) throws NoSuchMethodException {
Class<?> type = type();
// first priority: find a public method with exact signature match in class hierarchy
try {
return type.getMethod(name, types);
}
// second priority: find a private method with exact signature match on declaring class
catch (NoSuchMethodException e) {
do {
try {
return type.getDeclaredMethod(name, types);
}
catch(NoSuchMethodException ignore) {}
type = type.getSuperclass();
}
while(type != null);
throw new NoSuchMethodException();
}
}
/**
* Searches a method with a similar signature as desired using
* {@link #isSimilarSignature(java.lang.reflect.Method, String, Class[])}.
* <p>
* First public methods are searched in the class hierarchy, then private
* methods on the declaring class. If a method could be found, it is
* returned, otherwise a {@code NoSuchMethodException} is thrown.
*/
private Method similarMethod(String name, Class<?>[] types) throws NoSuchMethodException {
Class<?> type = type();
// first priority: find a public method with a "similar" signature in class hierarchy
// similar interpreted in when primitive argument types are converted to their wrappers
for (Method method : type.getMethods()) {
if (isSimilarSignature(method, name, types)) {
return method;
}
}
// second priority: find a non-public method with a "similar" signature on declaring class
do {
for(Method method : type.getDeclaredMethods()) {
if (isSimilarSignature(method, name, types)) {
return method;
}
}
type = type.getSuperclass();
}
while(type != null);
throw new NoSuchMethodException("No similar method " + name + " with params " + Arrays.toString(types) + " could be found on type " + type() + ".");
}
/**
* Determines if a method has a "similar" signature, especially if wrapping
* primitive argument types would result in an exactly matching signature.
*/
private boolean isSimilarSignature(Method possiblyMatchingMethod, String desiredMethodName, Class<?>[] desiredParamTypes) {
return possiblyMatchingMethod.getName().equals(desiredMethodName) && match(possiblyMatchingMethod.getParameterTypes(), desiredParamTypes);
}
/**
* Call a constructor.
* <p>
* This is a convenience method for calling
* <code>create(new Object[0])</code>
*
* @return The wrapped new object, to be used for further reflection.
* @throws ReflectException If any reflection exception occurred.
* @see #create(Object...)
*/
public Reflect create() throws ReflectException {
return create(new Object[0]);
}
/**
* Call a constructor.
* <p>
* This is roughly equivalent to {@link Constructor#newInstance(Object...)}.
* If the wrapped object is a {@link Class}, then this will create a new
* object of that class. If the wrapped object is any other {@link Object},
* then this will create a new object of the same type.
* <p>
* Just like {@link Constructor#newInstance(Object...)}, this will try to
* wrap primitive types or unwrap primitive type wrappers if applicable. If
* several constructors are applicable, by that rule, the first one
* encountered is called. i.e. when calling <code><pre>
* on(C.class).create(1, 1);
* </pre></code> The first of the following constructors will be applied:
* <code><pre>
* public C(int param1, Integer param2);
* public C(Integer param1, int param2);
* public C(Number param1, Number param2);
* public C(Number param1, Object param2);
* public C(int param1, Object param2);
* </pre></code>
*
* @param args The constructor arguments
* @return The wrapped new object, to be used for further reflection.
* @throws ReflectException If any reflection exception occurred.
*/
public Reflect create(Object... args) throws ReflectException {
Class<?>[] types = types(args);
// Try invoking the "canonical" constructor, i.e. the one with exact
// matching argument types
try {
Constructor<?> constructor = type().getDeclaredConstructor(types);
return on(constructor, args);
}
// If there is no exact match, try to find one that has a "similar"
// signature if primitive argument types are converted to their wrappers
catch (NoSuchMethodException e) {
for(Constructor<?> constructor : type().getDeclaredConstructors()) {
if (match(constructor.getParameterTypes(), types)) {
return on(constructor, args);
}
}
throw new ReflectException(e);
}
}
/**
* Create a proxy for the wrapped object allowing to typesafely invoke
* methods on it using a custom interface
*
* @param proxyType The interface type that is implemented by the proxy
* @return A proxy for the wrapped object
*/
@SuppressWarnings("unchecked")
public <P> P as(Class<P> proxyType) {
final boolean isMap = (object instanceof Map);
final InvocationHandler handler = new InvocationHandler() {
@SuppressWarnings("null")
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String name = method.getName();
// Actual method name matches always come first
try {
return on(object).call(name, args).get();
}
// [#14] Emulate POJO behaviour on wrapped map objects
catch (ReflectException e) {
if (isMap) {
Map<String, Object> map = (Map<String, Object>) object;
int length = (args == null ? 0 : args.length);
if (length == 0 && name.startsWith("get")) {
return map.get(property(name.substring(3)));
}
else if (length == 0 && name.startsWith("is")) {
return map.get(property(name.substring(2)));
}
else if (length == 1 && name.startsWith("set")) {
map.put(property(name.substring(3)), args[0]);
return null;
}
}
throw e;
}
}
};
return (P) Proxy.newProxyInstance(proxyType.getClassLoader(), new Class[] { proxyType }, handler);
}
/**
* Get the POJO property name of an getter/setter
*/
private static String property(String string) {
int length = string.length();
if (length == 0) {
return "";
}
else if (length == 1) {
return string.toLowerCase(Locale.ENGLISH);
}
else {
return string.substring(0, 1).toLowerCase(Locale.ENGLISH) + string.substring(1);
}
}
// ---------------------------------------------------------------------
// Object API
// ---------------------------------------------------------------------
/**
* Check whether two arrays of types match, converting primitive types to
* their corresponding wrappers.
*/
private boolean match(Class<?>[] declaredTypes, Class<?>[] actualTypes) {
if (declaredTypes.length == actualTypes.length) {
for (int i = 0; i < actualTypes.length; i++) {
if (actualTypes[i] == NULL.class)
continue;
if (wrapper(declaredTypes[i]).isAssignableFrom(wrapper(actualTypes[i])))
continue;
return false;
}
return true;
}
else {
return false;
}
}
/**
* {@inheritDoc}
*/
@Override
public int hashCode() {
return object.hashCode();
}
/**
* {@inheritDoc}
*/
@Override
public boolean equals(Object obj) {
if (obj instanceof Reflect) {
return object.equals(((Reflect) obj).get());
}
return false;
}
/**
* {@inheritDoc}
*/
@Override
public String toString() {
return object.toString();
}
// ---------------------------------------------------------------------
// Utility methods
// ---------------------------------------------------------------------
/**
* Wrap an object created from a constructor
*/
private static Reflect on(Constructor<?> constructor, Object... args) throws ReflectException {
try {
return on(accessible(constructor).newInstance(args));
}
catch (Exception e) {
throw new ReflectException(e);
}
}
/**
* Wrap an object returned from a method
*/
private static Reflect on(Method method, Object object, Object... args) throws ReflectException {
try {
accessible(method);
if (method.getReturnType() == void.class) {
method.invoke(object, args);
return on(object);
}
else {
return on(method.invoke(object, args));
}
}
catch (Exception e) {
throw new ReflectException(e);
}
}
/**
* Unwrap an object
*/
private static Object unwrap(Object object) {
if (object instanceof Reflect) {
return ((Reflect) object).get();
}
return object;
}
/**
* Get an array of types for an array of objects
*
* @see Object#getClass()
*/
private static Class<?>[] types(Object... values) {
if (values == null) {
return new Class[0];
}
Class<?>[] result = new Class[values.length];
for (int i = 0; i < values.length; i++) {
Object value = values[i];
result[i] = value == null ? NULL.class : value.getClass();
}
return result;
}
/**
* Load a class
*
* @see Class#forName(String)
*/
private static Class<?> forName(String name) throws ReflectException {
try {
return Class.forName(name);
}
catch (Exception e) {
throw new ReflectException(e);
}
}
private static Class<?> forName(String name, ClassLoader classLoader) throws ReflectException {
try {
return Class.forName(name, true, classLoader);
}
catch(Exception e) {
throw new ReflectException(e);
}
}
/**
* Get the type of the wrapped object.
*
* @see Object#getClass()
*/
public Class<?> type() {
if (isClass) {
return (Class<?>) object;
}
else {
return object.getClass();
}
}
/**
* Get a wrapper type for a primitive type, or the argument type itself, if
* it is not a primitive type.
*/
public static Class<?> wrapper(Class<?> type) {
if (type == null) {
return null;
}
else if (type.isPrimitive()) {
if (boolean.class == type) {
return Boolean.class;
}
else if (int.class == type) {
return Integer.class;
}
else if (long.class == type) {
return Long.class;
}
else if (short.class == type) {
return Short.class;
}
else if (byte.class == type) {
return Byte.class;
}
else if (double.class == type) {
return Double.class;
}
else if (float.class == type) {
return Float.class;
}
else if (char.class == type) {
return Character.class;
}
else if (void.class == type) {
return Void.class;
}
}
return type;
}
private static class NULL {}
}

View File

@@ -0,0 +1,79 @@
/**
* Copyright (c) 2011-2013, Lukas Eder, lukas.eder@gmail.com
* All rights reserved.
*
* This software is licensed to you under the Apache License, Version 2.0
* (the "License"); You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* . Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* . Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* . Neither the name "jOOR" nor the names of its contributors may be
* used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
package org.joor;
import java.lang.reflect.InvocationTargetException;
/**
* A unchecked wrapper for any of Java's checked reflection exceptions:
* <p>
* These exceptions are
* <ul>
* <li> {@link ClassNotFoundException}</li>
* <li> {@link IllegalAccessException}</li>
* <li> {@link IllegalArgumentException}</li>
* <li> {@link InstantiationException}</li>
* <li> {@link InvocationTargetException}</li>
* <li> {@link NoSuchMethodException}</li>
* <li> {@link NoSuchFieldException}</li>
* <li> {@link SecurityException}</li>
* </ul>
*
* @author Lukas Eder
*/
public class ReflectException extends RuntimeException {
/**
* Generated UID
*/
private static final long serialVersionUID = -6213149635297151442L;
public ReflectException(String message) {
super(message);
}
public ReflectException(String message, Throwable cause) {
super(message, cause);
}
public ReflectException() {
super();
}
public ReflectException(Throwable cause) {
super(cause);
}
}

133
old code/tray/src/qz/App.java Executable file
View File

@@ -0,0 +1,133 @@
package qz;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.core.Filter;
import org.apache.logging.log4j.core.appender.RollingFileAppender;
import org.apache.logging.log4j.core.appender.rolling.DefaultRolloverStrategy;
import org.apache.logging.log4j.core.appender.rolling.SizeBasedTriggeringPolicy;
import org.apache.logging.log4j.core.filter.ThresholdFilter;
import org.apache.logging.log4j.core.layout.PatternLayout;
import qz.build.provision.params.Phase;
import qz.common.Constants;
import qz.installer.Installer;
import qz.installer.certificate.CertificateManager;
import qz.installer.certificate.ExpiryTask;
import qz.installer.certificate.KeyPairWrapper;
import qz.installer.certificate.NativeCertificateInstaller;
import qz.installer.provision.ProvisionInstaller;
import qz.ui.PairingConfigDialog;
import qz.utils.*;
import qz.ws.PrintSocketServer;
import qz.ws.SingleInstanceChecker;
import qz.ws.substitutions.Substitutions;
import java.io.File;
import java.security.cert.X509Certificate;
import java.util.Properties;
public class App {
private static final Logger log = LogManager.getLogger(App.class);
private static Properties trayProperties = null;
public static void main(String ... args) {
ArgParser parser = new ArgParser(args);
LibUtilities.getInstance().bind();
if(parser.intercept()) {
FileUtilities.cleanup();
System.exit(parser.getExitCode());
}
SingleInstanceChecker.stealWebsocket = parser.hasFlag(ArgValue.STEAL);
setupFileLogging();
log.info(Constants.ABOUT_TITLE + " version: {}", Constants.VERSION);
log.info(Constants.ABOUT_TITLE + " vendor: {}", Constants.ABOUT_COMPANY);
log.info("Java version: {}", Constants.JAVA_VERSION.toString());
log.info("Java vendor: {}", Constants.JAVA_VENDOR);
Substitutions.setEnabled(PrefsSearch.getBoolean(ArgValue.SECURITY_SUBSTITUTIONS_ENABLE));
Substitutions.setStrict(PrefsSearch.getBoolean(ArgValue.SECURITY_SUBSTITUTIONS_STRICT));
CertificateManager certManager = null;
try {
// Gets and sets the SSL info, properties file
certManager = Installer.getInstance().certGen(false);
trayProperties = certManager.getProperties();
// Reoccurring (e.g. hourly) cert expiration check
new ExpiryTask(certManager).schedule();
} catch(Exception e) {
log.error("Something went critically wrong loading HTTPS", e);
}
Installer.getInstance().addUserSettings();
// Load overridable preferences set in qz-tray.properties file
NetworkUtilities.setPreferences(certManager.getProperties());
SingleInstanceChecker.setPreferences(certManager.getProperties());
// Linux needs the cert installed in user-space on every launch for Chrome SSL to work
if(!SystemUtilities.isWindows() && !SystemUtilities.isMac()) {
X509Certificate caCert = certManager.getKeyPair(KeyPairWrapper.Type.CA).getCert();
// Only install if a CA cert exists (e.g. one we generated)
if(caCert != null) {
NativeCertificateInstaller.getInstance().install(certManager.getKeyPair(KeyPairWrapper.Type.CA).getCert());
}
}
// Invoke any provisioning steps that are phase=startup
try {
ProvisionInstaller provisionInstaller = new ProvisionInstaller(SystemUtilities.getJarParentPath().resolve(Constants.PROVISION_DIR));
provisionInstaller.invoke(Phase.STARTUP);
} catch(Exception e) {
log.warn("An error occurred provisioning \"phase\": \"startup\" entries", e);
}
try {
log.info("Starting {} {}", Constants.ABOUT_TITLE, Constants.VERSION);
// Start the WebSocket
PrintSocketServer.runServer(certManager, parser.isHeadless());
}
catch(Exception e) {
log.error("Could not start tray manager", e);
}
FileUtilities.cleanup();
log.warn("The web socket server is no longer running");
// Show pairing config dialog if needed
PairingConfigDialog.showIfNeeded(null); // null for no parent frame, or pass your main frame if available
}
public static Properties getTrayProperties() {
return trayProperties;
}
private static void setupFileLogging() {
//disable jetty logging
System.setProperty("org.eclipse.jetty.util.log.class", "org.eclipse.jetty.util.log.StdErrLog");
System.setProperty("org.eclipse.jetty.LEVEL", "OFF");
if(PrefsSearch.getBoolean(ArgValue.LOG_DISABLE)) {
return;
}
int logSize = PrefsSearch.getInt(ArgValue.LOG_SIZE);
int logRotate = PrefsSearch.getInt(ArgValue.LOG_ROTATE);
Installer.getInstance().cleanupLegacyLogs(Math.max(logRotate, 5));
RollingFileAppender fileAppender = RollingFileAppender.newBuilder()
.setName("log-file")
.withAppend(true)
.setLayout(PatternLayout.newBuilder().withPattern("%d{ISO8601} [%p] %m%n").build())
.setFilter(ThresholdFilter.createFilter(Level.DEBUG, Filter.Result.ACCEPT, Filter.Result.DENY))
.withFileName(FileUtilities.USER_DIR + File.separator + Constants.LOG_FILE + ".log")
.withFilePattern(FileUtilities.USER_DIR + File.separator + Constants.LOG_FILE + ".%i.log")
.withStrategy(DefaultRolloverStrategy.newBuilder()
.withMax(String.valueOf(logRotate))
.withFileIndex("min")
.build())
.withPolicy(SizeBasedTriggeringPolicy.createPolicy(String.valueOf(logSize)))
.withImmediateFlush(true)
.build();
fileAppender.start();
LoggerUtilities.getRootLogger().addAppender(fileAppender);
}
}

View File

@@ -0,0 +1,70 @@
package qz.auth;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import qz.utils.ConnectionUtilities;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
/**
* Wrapper class for the Certificate Revocation List
* Created by Steven on 2/4/2015. Package: qz.auth Project: qz-print
*/
public class CRL {
private static final Logger log = LogManager.getLogger(CRL.class);
/** The URL to the QZ CRL. Should not be changed except for dev tests */
public static final String CRL_URL = "https://crl.qz.io";
private static CRL instance = null;
private ArrayList<String> revokedHashes = new ArrayList<String>();
private boolean loaded = false;
private CRL() {}
public static CRL getInstance() {
if (instance == null) {
instance = new CRL();
new Thread() {
@Override
public void run() {
log.info("Loading CRL from {}", CRL_URL);
try(BufferedReader br = new BufferedReader(new InputStreamReader(ConnectionUtilities.getInputStream(CRL_URL, false)))) {
String line;
while((line = br.readLine()) != null) {
//Ignore empty and commented lines
if (!line.isEmpty() && line.charAt(0) != '#') {
instance.revokedHashes.add(line);
}
}
instance.loaded = true;
log.info("Successfully loaded {} CRL entries from {}", instance.revokedHashes.size(), CRL_URL);
}
catch(IOException e) {
log.warn("Unable to access CRL from {}, {}", CRL_URL, e.toString());
}
}
}.start();
}
return instance;
}
public boolean isRevoked(String fingerprint) {
return revokedHashes.contains(fingerprint);
}
public boolean isLoaded() {
return loaded;
}
}

View File

@@ -0,0 +1,536 @@
package qz.auth;
import org.apache.commons.codec.binary.StringUtils;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.io.Charsets;
import org.apache.commons.ssl.Base64;
import org.apache.commons.ssl.X509CertificateChainBuilder;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.x500.style.BCStyle;
import org.bouncycastle.jce.PrincipalUtil;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import qz.App;
import qz.common.Constants;
import qz.utils.*;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.*;
import java.security.cert.*;
import java.time.DateTimeException;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.*;
/**
* Created by Steven on 1/27/2015. Package: qz.auth Project: qz-print
* Wrapper to store certificate objects from
*/
public class Certificate {
private static final Logger log = LogManager.getLogger(Certificate.class);
private static final String QUIETLY_FAIL = "quiet";
public enum Algorithm {
SHA1("SHA1withRSA"),
SHA256("SHA256withRSA"),
SHA512("SHA512withRSA");
String name;
Algorithm(String name) {
this.name = name;
}
}
public static ArrayList<Certificate> rootCAs = new ArrayList<>();
public static Certificate builtIn;
private static CertPathValidator validator;
private static CertificateFactory factory;
private static boolean trustBuiltIn = false;
// id-at-description used for storing renewal information
private static ASN1ObjectIdentifier RENEWAL_OF = new ASN1ObjectIdentifier("2.5.4.13");
public static final String[] saveFields = new String[] {"fingerprint", "commonName", "organization", "validFrom", "validTo", "valid"};
public static final String SPONSORED_CN_PREFIX = "Sponsored:";
// Valid date range allows UI to only show "Expired" text for valid certificates
private static final Instant UNKNOWN_MIN = LocalDateTime.MIN.toInstant(ZoneOffset.UTC);
private static final Instant UNKNOWN_MAX = LocalDateTime.MAX.toInstant(ZoneOffset.UTC);
public static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
public static final DateTimeFormatter DATE_PARSE = DateTimeFormatter.ofPattern("uuuu-MM-dd['T'][ ]HH:mm:ss[.n]['Z']"); //allow parsing of both ISO and custom formatted dates
private X509Certificate theCertificate;
private boolean sponsored;
private String fingerprint;
private String commonName;
private String organization;
private Instant validFrom;
private Instant validTo;
//used by review sites UI only
private boolean expired = false;
private boolean valid = false;
private boolean rootCA = false; // TODO: Move to constructor?
//Pre-set certificate for use when missing
public static final Certificate UNKNOWN;
static {
HashMap<String,String> map = new HashMap<>();
map.put("fingerprint", "UNKNOWN REQUEST");
map.put("commonName", "An anonymous request");
map.put("organization", "Unknown");
map.put("validFrom", UNKNOWN_MIN.toString());
map.put("validTo", UNKNOWN_MAX.toString());
map.put("valid", "false");
UNKNOWN = Certificate.loadCertificate(map);
}
static {
try {
Security.addProvider(new BouncyCastleProvider());
validator = CertPathValidator.getInstance("PKIX");
factory = CertificateFactory.getInstance("X.509");
builtIn = new Certificate("-----BEGIN CERTIFICATE-----\n" +
"MIIELzCCAxegAwIBAgIJALm151zCHDxiMA0GCSqGSIb3DQEBCwUAMIGsMQswCQYD\n" +
"VQQGEwJVUzELMAkGA1UECAwCTlkxEjAQBgNVBAcMCUNhbmFzdG90YTEbMBkGA1UE\n" +
"CgwSUVogSW5kdXN0cmllcywgTExDMRswGQYDVQQLDBJRWiBJbmR1c3RyaWVzLCBM\n" +
"TEMxGTAXBgNVBAMMEHF6aW5kdXN0cmllcy5jb20xJzAlBgkqhkiG9w0BCQEWGHN1\n" +
"cHBvcnRAcXppbmR1c3RyaWVzLmNvbTAgFw0xNTAzMDEyMzM4MjlaGA8yMTE1MDMw\n" +
"MjIzMzgyOVowgawxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJOWTESMBAGA1UEBwwJ\n" +
"Q2FuYXN0b3RhMRswGQYDVQQKDBJRWiBJbmR1c3RyaWVzLCBMTEMxGzAZBgNVBAsM\n" +
"ElFaIEluZHVzdHJpZXMsIExMQzEZMBcGA1UEAwwQcXppbmR1c3RyaWVzLmNvbTEn\n" +
"MCUGCSqGSIb3DQEJARYYc3VwcG9ydEBxemluZHVzdHJpZXMuY29tMIIBIjANBgkq\n" +
"hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuWsBa6uk+RM4OKBZTRfIIyqaaFD71FAS\n" +
"7kojAQ+ySMpYuqLjIVZuCh92o1FGBvyBKUFc6knAHw5749yhLCYLXhzWwiNW2ri1\n" +
"Jwx/d83Wnaw6qA3lt++u3tmiA8tsFtss0QZW0YBpFsIqhamvB3ypwu0bdUV/oH7g\n" +
"/s8TFR5LrDfnfxlLFYhTUVWuWzMqEFAGnFG3uw/QMWZnQgkGbx0LMcYzdqFb7/vz\n" +
"rTSHfjJsisUTWPjo7SBnAtNYCYaGj0YH5RFUdabnvoTdV2XpA5IPYa9Q597g/M0z\n" +
"icAjuaK614nKXDaAUCbjki8RL3OK9KY920zNFboq/jKG6rKW2t51ZQIDAQABo1Aw\n" +
"TjAdBgNVHQ4EFgQUA0XGTcD6jqkL2oMPQaVtEgZDqV4wHwYDVR0jBBgwFoAUA0XG\n" +
"TcD6jqkL2oMPQaVtEgZDqV4wDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOC\n" +
"AQEAijcT5QMVqrWWqpNEe1DidzQfSnKo17ZogHW+BfUbxv65JbDIntnk1XgtLTKB\n" +
"VAdIWUtGZbXxrp16NEsh96V2hjDIoiAaEpW+Cp6AHhIVgVh7Q9Knq9xZ1t6H8PL5\n" +
"QiYQKQgJ0HapdCxlPKBfUm/Mj1ppNl9mPFJwgHmzORexbxrzU/M5i2jlies+CXNq\n" +
"cvmF2l33QNHnLwpFGwYKs08pyHwUPp6+bfci6lRvavztgvnKroWWIRq9ZPlC0yVK\n" +
"FFemhbCd7ZVbrTo0NcWZM1PTAbvlOikV9eh3i1Vot+3dJ8F27KwUTtnV0B9Jrxum\n" +
"W9P3C48mvwTxYZJFOu0N9UBLLg==\n" +
"-----END CERTIFICATE-----");
builtIn.valid = true;
setTrustBuiltIn(true);
scanAdditionalCAs();
}
catch(NoSuchAlgorithmException | CertificateException e) {
e.printStackTrace();
}
}
public static void scanAdditionalCAs() {
ArrayList<Map.Entry<Path, String>> certPaths = new ArrayList<>();
// First, look for "authcert.override", "-DtrustedRootCert"
certPaths.addAll(FileUtilities.parseDelimitedPaths(PrefsSearch.getString(ArgValue.AUTHCERT_OVERRIDE, App.getTrayProperties())));
// Second, look for "override.crt" within App directory
certPaths.add(new AbstractMap.SimpleEntry<>(SystemUtilities.getJarParentPath().resolve(Constants.OVERRIDE_CERT), QUIETLY_FAIL));
for(Map.Entry<Path, String> certPath : certPaths) {
if(certPath.getKey() != null) {
if (certPath.getKey().toFile().exists()) {
try {
Certificate caCert = new Certificate(FileUtilities.readLocalFile(certPath.getKey()));
caCert.rootCA = true;
caCert.valid = true;
if(!rootCAs.contains(caCert)) {
log.debug("Adding CA certificate: CN={}, O={} ({})",
caCert.getCommonName(), caCert.getOrganization(), caCert.getFingerprint());
rootCAs.add(caCert);
} else {
log.warn("CA cert exists, skipping: {}", certPath.getKey());
}
}
catch(Exception e) {
log.error("Error loading CA cert: {}", certPath.getKey(), e);
}
} else if(!certPath.getValue().equals(QUIETLY_FAIL)) {
log.warn("CA cert \"{}\" was provided, but could not be found, skipping.", certPath.getKey());
}
}
}
}
public Certificate(Path path) throws IOException, CertificateException {
this(new String(Files.readAllBytes(path), Charsets.UTF_8));
}
/** Decodes a certificate and intermediate certificate from the given string */
public Certificate(String in) throws CertificateException {
try {
//Strip beginning and end
String[] split = in.split("--START INTERMEDIATE CERT--");
byte[] serverCertificate = Base64.decodeBase64(split[0].replaceAll(X509Constants.BEGIN_CERT, "").replaceAll(X509Constants.END_CERT, ""));
X509Certificate theIntermediateCertificate;
if (split.length == 2) {
byte[] intermediateCertificate = Base64.decodeBase64(split[1].replaceAll(X509Constants.BEGIN_CERT, "").replaceAll(X509Constants.END_CERT, ""));
theIntermediateCertificate = (X509Certificate)factory.generateCertificate(new ByteArrayInputStream(intermediateCertificate));
} else {
theIntermediateCertificate = null; //Self-signed
}
//Generate cert
theCertificate = (X509Certificate)factory.generateCertificate(new ByteArrayInputStream(serverCertificate));
commonName = getSubjectX509Principal(theCertificate, BCStyle.CN);
if(commonName.isEmpty()) {
throw new CertificateException("Common Name cannot be blank.");
}
// Remove "Sponsored: " from CN, we'll swap the trusted icon instead <3
if(commonName.startsWith(SPONSORED_CN_PREFIX)) {
commonName = commonName.split(SPONSORED_CN_PREFIX)[1].trim();
sponsored = true;
} else {
sponsored = false;
}
fingerprint = makeThumbPrint(theCertificate);
organization = getSubjectX509Principal(theCertificate, BCStyle.O);
validFrom = theCertificate.getNotBefore().toInstant();
validTo = theCertificate.getNotAfter().toInstant();
// Check trust anchor against all root certs
Certificate foundRoot = null;
if(!this.rootCA) {
for(Certificate rootCA : rootCAs) {
HashSet<X509Certificate> chain = new HashSet<>();
try {
chain.add(rootCA.theCertificate);
if (theIntermediateCertificate != null) { chain.add(theIntermediateCertificate); }
X509Certificate[] x509Certificates = X509CertificateChainBuilder.buildPath(theCertificate, chain);
Set<TrustAnchor> anchor = new HashSet<>();
anchor.add(new TrustAnchor(rootCA.theCertificate, null));
PKIXParameters params = new PKIXParameters(anchor);
params.setRevocationEnabled(false); // TODO: Re-enable, remove proprietary CRL
validator.validate(factory.generateCertPath(Arrays.asList(x509Certificates)), params);
foundRoot = rootCA;
valid = true;
log.debug("Successfully chained certificate: CN={}, O={} ({})", getCommonName(), getOrganization(), getFingerprint());
break; // if successful, don't attempt another chain
}
catch(Exception e) {
log.warn("Problem building certificate chain (normal if multiple CAs are in use)");
}
}
}
// Check for expiration
Instant now = Instant.now();
if (expired = (validFrom.isAfter(now) || validTo.isBefore(now))) {
log.warn("Certificate is expired: CN={}, O={} ({})", getCommonName(), getOrganization(), getFingerprint());
valid = false;
}
// If cert matches a rootCA trust it blindly
// If cert is chained to a 3rd party rootCA, trust it blindly as well
Iterator<Certificate> allCerts = rootCAs.iterator();
while(allCerts.hasNext()) {
Certificate cert = allCerts.next();
if(cert.equals(this) || (cert.equals(foundRoot) && !cert.equals(builtIn))) {
log.debug("Adding {} to {} list", cert.toString(), Constants.ALLOW_FILE);
if(!isSaved()) {
FileUtilities.printLineToFile(Constants.ALLOW_FILE, data());
}
valid = true;
break;
}
}
readRenewalInfo();
CRL qzCrl = CRL.getInstance();
if (qzCrl.isLoaded()) {
if (qzCrl.isRevoked(getFingerprint()) || (theIntermediateCertificate != null && qzCrl.isRevoked(makeThumbPrint(theIntermediateCertificate)))) {
log.error("Certificate has been revoked and can no longer be used: CN={}, O={} ({})", getCommonName(), getOrganization(), getFingerprint());
valid = false;
}
} else {
//Assume nothing is revoked, because we can't get the CRL
log.warn("Failed to retrieve QZ CRL, skipping CRL check");
}
}
catch(Exception e) {
CertificateException certificateException = new CertificateException();
certificateException.initCause(e);
throw certificateException;
}
}
private void readRenewalInfo() throws Exception {
Vector values = PrincipalUtil.getSubjectX509Principal(theCertificate).getValues(RENEWAL_OF);
Iterator renewals = values.iterator();
while(renewals.hasNext()) {
String renewalInfo = String.valueOf(renewals.next());
String renewalPrefix = "renewal-of-";
if (!renewalInfo.startsWith(renewalPrefix)) {
log.warn("Malformed renewal info: {}", renewalInfo);
continue;
}
String previousFingerprint = renewalInfo.substring(renewalPrefix.length());
if (previousFingerprint.length() != 40) {
log.warn("Malformed renewal fingerprint: {}", previousFingerprint);
continue;
}
// Add this certificate to the whitelist if the previous certificate was whitelisted
// First, handle shared directory
File sharedFile = FileUtilities.getFile(Constants.ALLOW_FILE, false);
if (existsInAnyFile(previousFingerprint, sharedFile) && !isSaved(false)) {
if(!FileUtilities.printLineToFile(Constants.ALLOW_FILE, data(), false)) {
// Fallback to local directory if shared is not writable
FileUtilities.printLineToFile(Constants.ALLOW_FILE, data(), /* fallback */ true);
}
}
// Second, handle local directory
File localFile = FileUtilities.getFile(Constants.ALLOW_FILE, true);
if (existsInAnyFile(previousFingerprint, localFile) && !isSaved(true)) {
FileUtilities.printLineToFile(Constants.ALLOW_FILE, data(), true);
}
}
}
private Certificate() {}
/**
* Used to rebuild a certificate for the 'Saved Sites' screen without having to decrypt the certificates again
*/
public static Certificate loadCertificate(HashMap<String,String> data) {
Certificate cert = new Certificate();
cert.fingerprint = data.get("fingerprint");
cert.commonName = data.get("commonName");
cert.organization = data.get("organization");
try {
cert.validFrom = Instant.from(LocalDateTime.from(DATE_PARSE.parse(data.get("validFrom"))).atZone(ZoneOffset.UTC));
cert.validTo = Instant.from(LocalDateTime.from(DATE_PARSE.parse(data.get("validTo"))).atZone(ZoneOffset.UTC));
}
catch(DateTimeException e) {
cert.validFrom = UNKNOWN_MIN;
cert.validTo = UNKNOWN_MAX;
log.warn("Unable to parse certificate date: {}", e.getMessage());
}
cert.valid = Boolean.parseBoolean(data.get("valid"));
return cert;
}
/**
* Checks given signature for given data against this certificate,
* ensuring it is properly signed
*
* @param signature the signature appended to the data, base64 encoded
* @param data the data to check
* @return true if signature valid, false if not
*/
public boolean isSignatureValid(Algorithm algorithm, String signature, String data) {
if (!signature.isEmpty()) {
//On errors, assume failure.
try {
Signature verifier = Signature.getInstance(algorithm.name);
verifier.initVerify(theCertificate.getPublicKey());
verifier.update(StringUtils.getBytesUtf8(DigestUtils.sha256Hex(data)));
return verifier.verify(Base64.decodeBase64(signature));
}
catch(GeneralSecurityException e) {
log.error("Unable to verify signature", e);
}
}
return false;
}
/** Checks if the certificate has been added to the specified allow file */
public boolean isSaved(boolean local) {
File allowed = FileUtilities.getFile(Constants.ALLOW_FILE, local);
return existsInAnyFile(getFingerprint(), allowed);
}
/** Checks if the certificate has been added to any allow file */
public boolean isSaved() {
return isSaved(false) || isSaved(true);
}
/** Checks if the certificate has been added to the local block file */
public boolean isBlocked() {
File blocks = FileUtilities.getFile(Constants.BLOCK_FILE, true);
File blocksShared = FileUtilities.getFile(Constants.BLOCK_FILE, false);
return existsInAnyFile(getFingerprint(), blocksShared, blocks);
}
private static boolean existsInAnyFile(String fingerprint, File... files) {
for(File file : files) {
if (file == null) { continue; }
try(BufferedReader br = new BufferedReader(new FileReader(file))) {
String line;
while((line = br.readLine()) != null) {
if (line.contains("\t")) {
String print = line.substring(0, line.indexOf("\t"));
if (print.equals(fingerprint)) {
return true;
}
}
}
}
catch(IOException e) {
e.printStackTrace();
}
}
return false;
}
public String getFingerprint() {
return fingerprint;
}
public String getCommonName() {
return commonName;
}
public String getOrganization() {
return organization;
}
public String getValidFrom() {
if (validFrom.isAfter(UNKNOWN_MIN)) {
return DATE_FORMAT.format(validFrom.atZone(ZoneOffset.UTC));
} else {
return "Not Provided";
}
}
public String getValidTo() {
if (validTo.isBefore(UNKNOWN_MAX)) {
return DATE_FORMAT.format(validTo.atZone(ZoneOffset.UTC));
} else {
return "Not Provided";
}
}
public Instant getValidFromDate() {
return validFrom;
}
public Instant getValidToDate() {
return validTo;
}
/**
* Validates certificate against embedded cert.
*/
public boolean isTrusted() {
return isValid() && !isExpired();
}
public boolean isSponsored() {
return sponsored;
}
public boolean isValid() {
return valid;
}
public boolean isExpired() {
return expired;
}
public static String makeThumbPrint(X509Certificate cert) throws NoSuchAlgorithmException, CertificateEncodingException {
MessageDigest md = MessageDigest.getInstance("SHA-1");
md.update(cert.getEncoded());
return ByteUtilities.bytesToHex(md.digest(), false);
}
private String data(boolean assumeTrusted) {
return getFingerprint() + "\t" +
getCommonName() + "\t" +
getOrganization() + "\t" +
getValidFrom() + "\t" +
getValidTo() + "\t" +
// Used by equals(), may fail if it hasn't been trusted yet
(assumeTrusted ? true : isTrusted());
}
public String data() {
return data(false);
}
@Override
public String toString() {
return getOrganization() + " (" + getCommonName() + ")";
}
@Override
public boolean equals(Object obj) {
if (obj instanceof Certificate) {
return ((Certificate)obj).data(true).equals(data(true));
}
return super.equals(obj);
}
public static void setTrustBuiltIn(boolean trustBuiltIn) {
if(trustBuiltIn) {
if (!rootCAs.contains(builtIn)) {
log.debug("Adding internal CA certificate: CN={}, O={} ({})",
builtIn.getCommonName(), builtIn.getOrganization(), builtIn.getFingerprint());
builtIn.rootCA = true;
builtIn.valid = true;
rootCAs.add(0, builtIn);
}
} else {
if (rootCAs.contains(builtIn)) {
log.debug("Removing internal CA certificate: CN={}, O={} ({})",
builtIn.getCommonName(), builtIn.getOrganization(), builtIn.getFingerprint());
rootCAs.remove(builtIn);
}
}
Certificate.trustBuiltIn = trustBuiltIn;
}
public static boolean isTrustBuiltIn() {
return trustBuiltIn;
}
public static boolean hasAdditionalCAs() {
return rootCAs.size() > (isTrustBuiltIn() ? 1 : 0);
}
private static String getSubjectX509Principal(X509Certificate cert, ASN1ObjectIdentifier key) {
try {
Vector v = PrincipalUtil.getSubjectX509Principal(cert).getValues(key);
if(v.size() > 0) {
return String.valueOf(v.get(0));
}
} catch(CertificateEncodingException e) {
log.warn("Certificate encoding exception occurred", e);
}
return "";
}
}

View File

@@ -0,0 +1,87 @@
package qz.auth;
import org.json.JSONObject;
import qz.utils.FileUtilities;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.File;
import java.io.FileReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.Base64;
public class PairingAuth {
private static String site = null;
private static String pairingKey = null;
private static final String HMAC_ALGO = "HmacSHA256";
private static File configFile = null;
static {
loadConfig();
}
private static void loadConfig() {
try {
// Use QZ Tray's user directory - same location as PairingConfigDialog
configFile = new File(FileUtilities.USER_DIR.toFile(), "pairing-config.json");
if (!configFile.exists()) {
System.out.println("Pairing config file not found at: " + configFile.getAbsolutePath());
site = null;
pairingKey = null;
return;
}
String content = new String(Files.readAllBytes(configFile.toPath()));
JSONObject config = new JSONObject(content);
site = config.optString("site", null);
pairingKey = config.optString("pairing_key", null);
System.out.println("Pairing config loaded from: " + configFile.getAbsolutePath());
System.out.println("Site configured: " + site);
} catch (Exception e) {
System.err.println("Error loading pairing config: " + e.getMessage());
e.printStackTrace();
site = null;
pairingKey = null;
}
}
public static String getSite() {
return site;
}
public static String getPairingKey() {
return pairingKey;
}
public static boolean isConfigured() {
return site != null && !site.isEmpty() && pairingKey != null && !pairingKey.isEmpty();
}
public static String getConfigFilePath() {
return configFile != null ? configFile.getAbsolutePath() : "unknown";
}
public static void reload() {
loadConfig();
}
public static String hmacSignature(String message) {
if (pairingKey == null || pairingKey.isEmpty()) {
System.err.println("Cannot compute HMAC: pairing key is not configured");
return null;
}
try {
Mac mac = Mac.getInstance(HMAC_ALGO);
SecretKeySpec keySpec = new SecretKeySpec(pairingKey.getBytes(StandardCharsets.UTF_8), HMAC_ALGO);
mac.init(keySpec);
byte[] hmac = mac.doFinal(message.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(hmac);
} catch (Exception e) {
System.err.println("HMAC computation failed: " + e.getMessage());
return null;
}
}
}

View File

@@ -0,0 +1,118 @@
package qz.auth;
import org.codehaus.jettison.json.JSONObject;
import qz.common.Constants;
import java.time.Instant;
import java.util.Arrays;
public class RequestState {
public enum Validity {
TRUSTED("Valid"),
EXPIRED("Expired Signature"),
UNSIGNED("Invalid Signature"),
EXPIRED_CERT("Expired Certificate"),
FUTURE_CERT("Future Certificate"),
INVALID_CERT("Invalid Certificate"),
UNKNOWN("Invalid");
private String formatted;
Validity(String formatted) {
this.formatted = formatted;
}
public String getFormatted() {
return formatted;
}
}
Certificate certUsed;
JSONObject requestData;
boolean initialConnect;
Validity status;
public RequestState(Certificate cert, JSONObject data) {
certUsed = cert;
requestData = data;
status = Validity.UNKNOWN;
}
public Certificate getCertUsed() {
return certUsed;
}
public JSONObject getRequestData() {
return requestData;
}
public boolean isInitialConnect() {
return initialConnect;
}
public void markNewConnection(Certificate cert) {
certUsed = cert;
initialConnect = true;
checkCertificateState(cert);
}
public void checkCertificateState(Certificate cert) {
if (cert.isTrusted()) {
status = Validity.TRUSTED;
} else if (cert.getValidToDate().isBefore(Instant.now())) {
status = Validity.EXPIRED_CERT;
} else if (cert.getValidFromDate().isAfter(Instant.now())) {
status = Validity.FUTURE_CERT;
} else if (!cert.isValid()) {
status = Validity.INVALID_CERT;
} else {
status = Validity.UNKNOWN;
}
}
public Validity getStatus() {
return status;
}
public void setStatus(Validity state) {
status = state;
}
public boolean hasCertificate() {
return certUsed != null && certUsed != Certificate.UNKNOWN;
}
public boolean hasSavedCert() {
return isVerified() && certUsed.isSaved();
}
public boolean hasBlockedCert() {
return certUsed == null || certUsed.isBlocked();
}
public String getCertName() {
return certUsed.getCommonName();
}
public boolean isVerified() {
return certUsed.isTrusted() && status == Validity.TRUSTED;
}
public boolean isSponsored() {
return certUsed.isSponsored();
}
public String getValidityInfo() {
if (status == Validity.TRUSTED) {
return Constants.TRUSTED_CERT;
} else if (Arrays.asList(Validity.UNSIGNED, Validity.EXPIRED, Validity.EXPIRED_CERT, Validity.FUTURE_CERT).contains(status)) {
return Constants.NO_TRUST + " - " + status.getFormatted();
} else {
return Constants.UNTRUSTED_CERT;
}
}
}

View File

@@ -0,0 +1,12 @@
package qz.auth;
/**
* Created by Tres on 3/3/2015.
*/
public class X509Constants {
public static final String BEGIN_CERT = "-----BEGIN CERTIFICATE-----";
public static final String END_CERT = "-----END CERTIFICATE-----";
public static final String INTERMEDIATE_CERT = "--START INTERMEDIATE CERT--";
public static final String BEGIN_CRL = "-----BEGIN X509 CRL-----";
public static final String END_CRL = "-----END X509 CRL-----";
}

View File

@@ -0,0 +1,129 @@
package qz.build;
import org.apache.commons.io.FileUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import qz.utils.ShellUtilities;
import qz.utils.SystemUtilities;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
/**
* Fetches a zip or tarball from URL and decompresses it
*/
public class Fetcher {
public enum Format {
ZIP(".zip"),
TARBALL(".tar.gz"),
UNKNOWN(null);
String suffix;
Format(String suffix) {
this.suffix = suffix;
}
public String getSuffix() {
return suffix;
}
public static Format parse(String url) {
for(Format format : Format.values()) {
if (url.endsWith(format.getSuffix())) {
return format;
}
}
return UNKNOWN;
}
}
private static final Logger log = LogManager.getLogger(Fetcher.class);
public static void main(String ... args) throws IOException {
new Fetcher("jlink/qz-tray-src_x.x.x", "https://github.com/qzind/tray/archive/master.tar.gz").fetch().uncompress();
}
String resourceName;
String url;
Format format;
Path rootDir;
File tempArchive;
File tempExtracted;
File extracted;
public Fetcher(String resourceName, String url) {
this.url = url;
this.resourceName = resourceName;
this.format = Format.parse(url);
// Try to calculate out/
this.rootDir = SystemUtilities.getJarParentPath().getParent();
}
@SuppressWarnings("unused")
public Fetcher(String resourceName, String url, Format format, String rootDir) {
this.resourceName = resourceName;
this.url = url;
this.format = format;
this.rootDir = Paths.get(rootDir);
}
public Fetcher fetch() throws IOException {
extracted = new File(rootDir.toString(), resourceName);
if(extracted.isDirectory() && extracted.exists()) {
log.info("Resource '{}' from [{}] has already been downloaded and extracted. Using: [{}]", resourceName, url, extracted);
} else {
tempExtracted = new File(rootDir.toString(), resourceName + "~tmp");
if(tempExtracted.exists()) {
FileUtils.deleteDirectory(tempExtracted);
}
// temp directory to thwart partial extraction
tempExtracted.mkdirs();
tempArchive = File.createTempFile(resourceName, ".zip");
log.info("Fetching '{}' from [{}] and saving to [{}]", resourceName, url, tempArchive);
FileUtils.copyURLToFile(new URL(url), tempArchive);
}
return this;
}
public String uncompress() throws IOException {
if(tempArchive != null) {
log.info("Unzipping '{}' from [{}] to [{}]", resourceName, tempArchive, tempExtracted);
if(format == Format.ZIP) {
unzip(tempArchive.getAbsolutePath(), tempExtracted);
} else {
untar(tempArchive.getAbsolutePath(), tempExtracted);
}
log.info("Moving [{}] to [{}]", tempExtracted, extracted);
tempExtracted.renameTo(extracted);
}
return extracted.toString();
}
public static void untar(String sourceFile, File targetDir) throws IOException {
// TODO: Switch to TarArchiveInputStream from Apache Commons Compress
if (!ShellUtilities.execute("tar", "-xzf", sourceFile, "-C", targetDir.getPath())) {
throw new IOException("Something went wrong extracting " + sourceFile +", check logs for details");
}
}
public static void unzip(String sourceFile, File targetDir) throws IOException {
try (ZipInputStream zipIn = new ZipInputStream(new FileInputStream(sourceFile))) {
for (ZipEntry ze; (ze = zipIn.getNextEntry()) != null; ) {
Path resolvedPath = targetDir.toPath().resolve(ze.getName());
if (ze.isDirectory()) {
Files.createDirectories(resolvedPath);
} else {
Files.createDirectories(resolvedPath.getParent());
Files.copy(zipIn, resolvedPath);
}
}
}
}
}

View File

@@ -0,0 +1,341 @@
/**
* @author Tres Finocchiaro
*
* Copyright (C) 2020 Tres Finocchiaro, QZ Industries, LLC
*
* LGPL 2.1 This is free software. This software and source code are released under
* the "LGPL 2.1 License". A copy of this license should be distributed with
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
*/
package qz.build;
import com.github.zafarkhaja.semver.Version;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import qz.build.jlink.Platform;
import qz.build.jlink.Vendor;
import qz.build.jlink.Url;
import qz.build.provision.params.Arch;
import qz.common.Constants;
import qz.utils.*;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.file.*;
import java.util.*;
public class JLink {
private static final Logger log = LogManager.getLogger(JLink.class);
public static final Vendor JAVA_DEFAULT_VENDOR = Vendor.BELLSOFT;
private static final String JAVA_DEFAULT_VERSION = "11.0.17+7";
private static final String JAVA_DEFAULT_GC_ENGINE = "hotspot"; // or "openj9"
private static final String JAVA_DEFAULT_GC_VERSION = "0.35.0"; // openj9 gc only
private Path jarPath;
private Path jdepsPath;
private Path jlinkPath;
private Path jmodsPath;
private Path outPath;
private Version jdepsVersion;
private Platform hostPlatform;
private Platform targetPlatform;
private Arch hostArch;
private Arch targetArch;
private Vendor javaVendor;
private String gcEngine;
private String javaVersion;
private String gcVersion;
private Path targetJdk;
private Version javaSemver;
private LinkedHashSet<String> depList;
public JLink(String targetPlatform, String targetArch, String javaVendor, String javaVersion, String gcEngine, String gcVersion, String targetJdk) throws IOException {
this.hostPlatform = Platform.getCurrentPlatform();
this.hostArch = SystemUtilities.getArch();
this.targetPlatform = Platform.parse(targetPlatform, this.hostPlatform);
this.targetArch = Arch.parse(targetArch, this.hostArch);
this.javaVendor = Vendor.parse(javaVendor, JAVA_DEFAULT_VENDOR);
this.gcEngine = getParam("gcEngine", gcEngine, JAVA_DEFAULT_GC_ENGINE);
this.javaVersion = getParam("javaVersion", javaVersion, JAVA_DEFAULT_VERSION);
this.gcVersion = getParam("gcVersion", gcVersion, JAVA_DEFAULT_GC_VERSION);
this.javaSemver = SystemUtilities.getJavaVersion(this.javaVersion);
// Optional: Provide the location of a custom JDK on the local filesystem
if(!StringUtils.isEmpty(targetJdk)) {
Path jdkPath = Paths.get(targetJdk);
Properties jdkProps = new Properties();
jdkProps.load(new FileInputStream(jdkPath.resolve("release").toFile()));
String customVersion = jdkProps.getProperty("JAVA_VERSION");
if(customVersion.contains("\"")) {
customVersion = customVersion.split("\"")[1];
}
Version customSemver = SystemUtilities.getJavaVersion(customVersion);
if(needsDownload(javaSemver, customSemver)) {
// The "release" file doesn't have build info, so we can't auto-download :(
if(javaSemver.getMajorVersion() != customSemver.getMajorVersion()) {
log.error("Error: jlink version {}.0 does not match target java.base version {}.0", javaSemver.getMajorVersion(), customSemver.getMajorVersion());
} else {
// Handle edge-cases (e.g. JDK-8240734)
log.error("Error: jlink version {} is incompatible with target java.base version {}", javaSemver.getMajorVersion(), customSemver.getMajorVersion());
}
System.exit(2);
}
this.targetJdk = Paths.get(targetJdk);
calculateToolPaths(Paths.get(targetJdk));
} else {
// Determine if the version we're building with is compatible with the target version
if (needsDownload(javaSemver, Constants.JAVA_VERSION)) {
log.warn("Java versions are incompatible, locating a suitable runtime for Java " + javaSemver.getMajorVersion() + "...");
String hostJdk = downloadJdk(this.hostArch, this.hostPlatform);
calculateToolPaths(Paths.get(hostJdk));
} else {
calculateToolPaths(null);
}
}
if(this.targetJdk == null) {
targetJdk = downloadJdk(this.targetArch, this.targetPlatform);
jmodsPath = Paths.get(targetJdk, "jmods");
} else {
log.info("\"targetjdk\" was provided {}, skipping download", targetJdk);
jmodsPath = this.targetJdk.resolve("jmods");
}
log.info("Selecting jmods folder: {}", jmodsPath);
calculateJarPath()
.calculateOutPath()
.calculateDepList()
.deployJre();
}
public static void main(String ... args) throws IOException {
new JLink(null, null, null, null, null, null, null).calculateJarPath();
}
/**
* Handle incompatibilities between JDKs, download a fresh one if needed
*/
private static boolean needsDownload(Version want, Version installed) {
// jdeps and jlink historically require matching major JDK versions. Download if needed.
boolean downloadJdk = installed.getMajorVersion() != want.getMajorVersion();
// Per JDK-8240734: Major versions checks aren't enough starting with 11.0.16+8
// see also https://github.com/adoptium/adoptium-support/issues/557
Version bad = SystemUtilities.getJavaVersion("11.0.16+8");
if(want.greaterThanOrEqualTo(bad) && installed.lessThan(bad) ||
installed.greaterThanOrEqualTo(bad) && want.lessThan(bad)) {
// Force download
// Fixes "Hash of java.rmi differs from expected hash"
downloadJdk = true;
}
return downloadJdk;
}
/**
* Download the JDK and return the path it was extracted to
*/
private String downloadJdk(Arch arch, Platform platform) throws IOException {
String url = new Url(this.javaVendor).format(arch, platform, this.gcEngine, this.javaSemver, this.javaVersion, this.gcVersion);
// Saves to out e.g. "out/jlink/jdk-AdoptOpenjdk-amd64-platform-11_0_7"
String extractedJdk = new Fetcher(String.format("jlink/jdk-%s-%s-%s-%s", javaVendor.value(), arch, platform.value(), javaSemver.toString().replaceAll("\\+", "_")), url)
.fetch()
.uncompress();
// Get first subfolder, e.g. jdk-11.0.7+10
for(File subfolder : new File(extractedJdk).listFiles(pathname -> pathname.isDirectory())) {
extractedJdk = subfolder.getPath();
if(platform == Platform.MAC && Paths.get(extractedJdk, "/Contents/Home").toFile().isDirectory()) {
extractedJdk += "/Contents/Home";
}
log.info("Selecting JDK home: {}", extractedJdk);
break;
}
return extractedJdk;
}
private JLink calculateJarPath() {
if(SystemUtilities.isJar()) {
jarPath = SystemUtilities.getJarPath();
} else {
// Detect out/dist/qz-tray.jar for IDE usage
jarPath = SystemUtilities.getJarParentPath()
.resolve("dist")
.resolve(Constants.PROPS_FILE + ".jar");
}
log.info("Assuming jar path: {}", jarPath);
return this;
}
private JLink calculateOutPath() {
switch(targetPlatform) {
case MAC:
outPath = jarPath.resolve("../Java.runtime/Contents/Home").normalize();
break;
default:
outPath = jarPath.resolve("../runtime").normalize();
}
log.info("Assuming output path: {}", outPath);
return this;
}
private JLink calculateToolPaths(Path javaHome) throws IOException {
if(javaHome == null) {
javaHome = Paths.get(System.getProperty("java.home"));
}
log.info("Using JAVA_HOME: {}", javaHome);
jdepsPath = javaHome.resolve("bin").resolve(SystemUtilities.isWindows() ? "jdeps.exe" : "jdeps").normalize();
jlinkPath = javaHome.resolve("bin").resolve(SystemUtilities.isWindows() ? "jlink.exe" : "jlink").normalize();
log.info("Assuming jdeps path: {}", jdepsPath);
log.info("Assuming jlink path: {}", jlinkPath);
jdepsPath.toFile().setExecutable(true, false);
jlinkPath.toFile().setExecutable(true, false);
jdepsVersion = SystemUtilities.getJavaVersion(jdepsPath);
return this;
}
private JLink calculateDepList() throws IOException {
log.info("Calling jdeps to determine runtime dependencies");
depList = new LinkedHashSet<>();
// JDK11.0.11+requires suppressing of missing deps
String raw = jdepsVersion.compareTo(Version.valueOf("11.0.10")) > 0 ?
ShellUtilities.executeRaw(jdepsPath.toString(), "--multi-release", "9", "--list-deps", "--ignore-missing-deps", jarPath.toString()) :
ShellUtilities.executeRaw(jdepsPath.toString(), "--multi-release", "9", "--list-deps", jarPath.toString());
if (raw == null || raw.trim().isEmpty() || raw.trim().startsWith("Warning") ) {
throw new IOException("An unexpected error occurred calling jdeps. Please check the logs for details.\n" + raw);
}
for(String item : raw.split("\\r?\\n")) {
item = item.trim();
if(!item.isEmpty()) {
if(item.startsWith("JDK") || item.startsWith("jdk8internals")) {
// Remove e.g. "JDK removed internal API/sun.reflect"
log.trace("Removing dependency: '{}'", item);
continue;
}
if(item.contains("/")) {
// Isolate base name e.g. "java.base/com.sun.net.ssl"
item = item.split("/")[0];
}
depList.add(item);
}
}
switch(targetPlatform) {
case WINDOWS:
// Java accessibility bridge dependency, see https://github.com/qzind/tray/issues/1234
depList.add("jdk.accessibility");
default:
// Adds "bin/jcmd"
depList.add("jdk.jcmd");
// "jar:" URLs create transient zipfs dependency, see https://stackoverflow.com/a/57846672/3196753
depList.add("jdk.zipfs");
// fix for https://github.com/qzind/tray/issues/894 solution from https://github.com/adoptium/adoptium-support/issues/397
depList.add("jdk.crypto.ec");
}
return this;
}
private JLink deployJre() throws IOException {
if(targetPlatform == Platform.MAC) {
// Deploy Contents/MacOS/libjli.dylib
Path macOS = Files.createDirectories(outPath.resolve("../MacOS").normalize());
Path jliLib = macOS.resolve("libjli.dylib");
log.info("Deploying {}", macOS);
try {
// Not all jdks use a bundle format, but try this first
Files.copy(jmodsPath.resolve("../../MacOS/libjli.dylib").normalize(), jliLib, StandardCopyOption.REPLACE_EXISTING);
} catch(IOException ignore) {
// Fallback to flat format
String libjli = "../lib/jli/libjli.dylib";
if(javaSemver.getMajorVersion() >= 21) {
libjli = "../lib/libjli.dylib";
}
Files.copy(jmodsPath.resolve(libjli).normalize(), jliLib, StandardCopyOption.REPLACE_EXISTING);
}
// Deploy Contents/Info.plist
HashMap<String, String> fieldMap = new HashMap<>();
fieldMap.put("%BUNDLE_ID%", MacUtilities.getBundleId() + ".jre"); // e.g. io.qz.qz-tray.jre
fieldMap.put("%BUNDLE_VERSION%", String.format("%s.%s.%s", javaSemver.getMajorVersion(), javaSemver.getMinorVersion(), javaSemver.getPatchVersion()));
fieldMap.put("%BUNDLE_VERSION_FULL%", javaSemver.toString());
fieldMap.put("%BUNDLE_VENDOR%", javaVendor.getVendorName());
fieldMap.put("%BUNDLE_PRODUCT%", javaVendor.getProductName());
log.info("Deploying {}/Info.plist", macOS.getParent());
FileUtilities.configureAssetFile("assets/mac-runtime.plist.in", macOS.getParent().resolve("Info.plist"), fieldMap, JLink.class);
}
FileUtils.deleteQuietly(outPath.toFile());
if(ShellUtilities.execute(jlinkPath.toString(),
"--strip-debug",
"--compress=2",
"--no-header-files",
"--no-man-pages",
"--exclude-files=glob:**/legal/**",
"--module-path", jmodsPath.toString(),
"--add-modules", String.join(",", depList),
"--output", outPath.toString())) {
log.info("Successfully deployed a jre to {}", outPath);
// Remove all but java/javaw
List<String> keepFiles = new ArrayList<>();
//String[] keepFiles;
String keepExt;
switch(targetPlatform) {
case WINDOWS:
keepFiles.add("java.exe");
keepFiles.add("javaw.exe");
keepFiles.add("jcmd.exe");
if(depList.contains("jdk.accessibility")) {
// Java accessibility bridge switching tool
keepFiles.add("jabswitch.exe");
}
// Windows stores ".dll" files in bin
keepExt = ".dll";
break;
default:
keepFiles.add("java");
keepFiles.add("jcmd");
keepExt = null;
}
Files.list(outPath.resolve("bin")).forEach(binFile -> {
if(Files.isDirectory(binFile) || (keepExt != null && binFile.toString().endsWith(keepExt))) {
log.info("Keeping {}", binFile);
return; // iterate forEach
}
for(String name : keepFiles) {
if (binFile.endsWith(name)) {
log.info("Keeping {}", binFile);
return; // iterate forEach
}
}
log.info("Deleting {}", binFile);
binFile.toFile().delete();
});
return this;
}
throw new IOException("An error occurred deploying the jre. Please check the logs for details.");
}
public static String getParam(String paramName, String value, String fallback) {
if(value != null && !value.isEmpty() && !value.trim().isEmpty()) {
return value;
}
log.info("No {} specified, assuming '{}'", paramName, fallback);
return fallback;
}
}

View File

@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>English</string>
<key>CFBundleExecutable</key>
<string>libjli.dylib</string>
<key>CFBundleGetInfoString</key>
<string>%BUNDLE_VENDOR% %BUNDLE_PRODUCT% %BUNDLE_VERSION_FULL%</string>
<key>CFBundleIdentifier</key>
<string>%BUNDLE_ID%</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>7.0</string>
<key>CFBundleName</key>
<string>Java Runtime Image</string>
<key>CFBundlePackageType</key>
<string>BNDL</string>
<key>CFBundleShortVersionString</key>
<string>%BUNDLE_VERSION%</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>%BUNDLE_VERSION%</string>
<key>JavaVM</key>
<dict>
<key>JVMCapabilities</key>
<array>
<string>CommandLine</string>
<string>JNI</string>
<string>BundledApp</string>
</array>
<key>JVMMinimumFrameworkVersion</key>
<string>17.0.0</string>
<key>JVMMinimumSystemVersion</key>
<string>10.6.0</string>
<key>JVMPlatformVersion</key>
<string>%BUNDLE_VERSION_FULL%</string>
<key>JVMVendor</key>
<string>%BUNDLE_VENDOR%</string>
<key>JVMVersion</key>
<string>%BUNDLE_VERSION%</string>
</dict>
</dict>
</plist>

View File

View File

@@ -0,0 +1,68 @@
package qz.build.jlink;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.lang.reflect.Field;
import java.util.Locale;
/**
* A special template class for handling enums with varargs needing string matches.
*
* Parsable enums must declare <code>public static void String[] matches;</code>
* in the constructor, which <code>parse(Class enumType, </T>String value) will
* call using reflection.
*
* Enums are inherently static in Java and cannot extend superclasses. The
* workaround to avoid code duplication is to leverage reflection and generics in
* static utility functions.
*
* The downsides of this are:
* - Reflection is slow
* - Static helpers must be explicitly class-type-aware*
*
* *Non-static methods may be implicit, but create anti-patterns for static helpers
* such as <code>parse(String value)</code> as they would exist at
* <code>ENUM_ENTRY.parse(...)</code> rather than <code>EnumClass.parse(...)</code>.
*/
public interface Parsable<T extends Enum> {
Logger log = LogManager.getLogger(Parsable.class);
static <T extends Enum<T>> T parse(Class<T> enumType, String value) {
if(value != null && !value.trim().isEmpty()) {
for(T parsable : enumType.getEnumConstants()) {
try {
Field matchesField = parsable.getClass().getDeclaredField("matches");
String[] matches = (String[])matchesField.get(parsable);
for(String match : matches) {
if (match.equalsIgnoreCase(value)) {
return parsable;
}
}
} catch(NoSuchFieldException | IllegalAccessException | ClassCastException e) {
log.warn("Parsable enums must have a 'public String[] matches' field", e);
}
}
}
log.warn("Could not parse {} as a valid {} value", value, enumType.getSimpleName());
return null;
}
static <T extends Enum<T>> T parse(Class<T> enumType, String value, T fallback, boolean silent) {
if(value != null && !value.trim().isEmpty()) {
return parse(enumType, value);
}
if(!silent) {
log.warn("No {} specified, assuming '{}'", enumType.getSimpleName(), ((Parsable)fallback).value());
}
return fallback;
}
static <T extends Enum<T>> T parse(Class<T> enumType, String value, T fallback) {
return parse(enumType, value, fallback, false);
}
default String value() {
return ((T)this).toString().toLowerCase(Locale.ENGLISH);
}
}

View File

@@ -0,0 +1,37 @@
package qz.build.jlink;
import qz.utils.SystemUtilities;
/**
* Handling of platform names as they would appear in a URL
* Values added must also be added to <code>ArgValue.JLINK --platform</code> values
*/
public enum Platform implements Parsable {
MAC("mac"),
WINDOWS("windows"),
LINUX("linux");
public final String[] matches;
Platform(String ... matches) { this.matches = matches; }
public static Platform parse(String value, Platform fallback) {
return Parsable.parse(Platform.class, value, fallback);
}
public static Platform parse(String value) {
return Parsable.parse(Platform.class, value);
}
public static Platform getCurrentPlatform() {
switch(SystemUtilities.getOs()) {
case MAC:
return Platform.MAC;
case WINDOWS:
return Platform.WINDOWS;
case LINUX:
default:
return Platform.LINUX;
}
}
}

View File

@@ -0,0 +1,72 @@
package qz.build.jlink;
import com.github.zafarkhaja.semver.Version;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import qz.build.provision.params.Arch;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.HashMap;
import static qz.build.jlink.Vendor.*;
/**
* Each JDK provider uses their own url format
*/
public class Url {
static HashMap<Vendor, String> VENDOR_URL_MAP = new HashMap<>();
static {
VENDOR_URL_MAP.put(BELLSOFT, "https://download.bell-sw.com/java/%s/bellsoft-jdk%s-%s-%s.%s");
VENDOR_URL_MAP.put(ECLIPSE, "https://github.com/adoptium/temurin%s-binaries/releases/download/jdk-%s/OpenJDK%sU-jdk_%s_%s_%s_%s.%s");
VENDOR_URL_MAP.put(IBM, "https://github.com/ibmruntimes/semeru%s-binaries/releases/download/jdk-%s_%s-%s/ibm-semeru-open-jdk_%s_%s_%s_%s-%s.%s");
VENDOR_URL_MAP.put(MICROSOFT, "https://aka.ms/download-jdk/microsoft-jdk-%s-%s-%s.%s");
VENDOR_URL_MAP.put(AMAZON, "https://corretto.aws/downloads/resources/%s/amazon-corretto-%s-%s-%s.%s");
VENDOR_URL_MAP.put(AZUL, "https://cdn.azul.com/zulu%s/bin/zulu%s-ca-jdk%s-%s_%s.%s");
}
private static final Logger log = LogManager.getLogger(Url.class);
Vendor vendor;
String pattern;
public Url(Vendor vendor) {
this.vendor = vendor;
if(!VENDOR_URL_MAP.containsKey(vendor)) {
throw new UnsupportedOperationException(String.format("Vendor provided '%s' couldn't be matched to a URL pattern, aborting.", vendor));
}
pattern = VENDOR_URL_MAP.get(vendor);
}
public String format(Arch arch, Platform platform, String gcEngine, Version javaSemver, String javaVersion, String gcVer) throws UnsupportedEncodingException {
Url pattern = new Url(vendor);
String urlArch = vendor.getUrlArch(arch);
String fileExt = vendor.getUrlExtension(platform);
String urlPlatform = vendor.getUrlPlatform(platform);
String urlJavaVersion = vendor.getUrlJavaVersion(javaSemver);
// Convert "+" to "%2B"
String urlJavaVersionEncode = URLEncoder.encode(javaVersion, "UTF-8");
int javaMajor = javaSemver.getMajorVersion();
switch(vendor) {
case BELLSOFT:
return String.format(pattern.pattern, urlJavaVersionEncode, urlJavaVersionEncode, urlPlatform, urlArch, fileExt);
case ECLIPSE:
return String.format(pattern.pattern, javaMajor, urlJavaVersionEncode, javaMajor, urlArch, urlPlatform, gcEngine, urlJavaVersion, fileExt);
case IBM:
return String.format(pattern.pattern, javaMajor, urlJavaVersionEncode, gcEngine, gcVer, urlArch, urlPlatform, urlJavaVersion, gcEngine, gcVer, fileExt);
case MICROSOFT:
return String.format(pattern.pattern, urlJavaVersion, urlPlatform, urlArch, fileExt);
case AMAZON:
return String.format(pattern.pattern, urlJavaVersion, urlJavaVersion, urlPlatform, urlArch, fileExt);
case AZUL:
// Special handling of Linux aarch64
String embedded = platform == Platform.LINUX ? "-embedded" : "";
return String.format(pattern.pattern, embedded, gcVer, urlJavaVersion, urlPlatform, urlArch, fileExt);
default:
throw new UnsupportedOperationException(String.format("URL pattern for '%s' (%s) is missing a format implementation.", vendor, pattern));
}
}
}

View File

@@ -0,0 +1,155 @@
package qz.build.jlink;
import com.github.zafarkhaja.semver.Version;
import qz.build.provision.params.Arch;
/**
* Handling of java vendors
*/
public enum Vendor implements Parsable {
ECLIPSE("Eclipse", "Adoptium", "adoptium", "temurin", "adoptopenjdk"),
BELLSOFT("BellSoft", "Liberica", "bellsoft", "liberica"),
IBM("IBM", "Semeru", "ibm", "semeru"),
MICROSOFT("Microsoft", "OpenJDK", "microsoft"),
AMAZON("Amazon", "Corretto", "amazon", "corretto"),
AZUL("Azul", "Zulu", "azul", "zulu");
public String vendorName;
public String productName;
public final String[] matches;
Vendor(String vendorName, String productName, String ... matches) {
this.matches = matches;
this.vendorName = vendorName;
this.productName = productName;
}
public static Vendor parse(String value, Vendor fallback) {
return Parsable.parse(Vendor.class, value, fallback, true);
}
public static Vendor parse(String value) {
return Parsable.parse(Vendor.class, value);
}
public String getVendorName() {
return vendorName;
}
public String getProductName() {
return productName;
}
/**
* Map Vendor to Arch value
*/
public String getUrlArch(Arch arch) {
switch(arch) {
case AARCH64:
// All vendors seem to use "aarch64" universally
return "aarch64";
case ARM32:
switch(this) {
case BELLSOFT:
return "arm32-vfp-hflt";
case AZUL:
return "aarch32hf";
case MICROSOFT:
case IBM:
throw new UnsupportedOperationException("Vendor does not provide builds for this architecture");
case AMAZON:
case ECLIPSE:
default:
return "arm";
}
case RISCV64:
return "riscv64";
case X86:
switch(this) {
case AZUL:
return "i686";
case BELLSOFT:
return "i586";
case ECLIPSE:
case IBM:
return "x86-32";
case AMAZON:
default:
return "x86";
}
case X86_64:
default:
switch(this) {
// BellSoft uses "amd64"
case BELLSOFT:
return "amd64";
}
return "x64";
}
}
/**
* Map Vendor to Platform name
*/
public String getUrlPlatform(Platform platform) {
switch(platform) {
case MAC:
switch(this) {
case BELLSOFT:
return "macos";
case MICROSOFT:
return "macOS";
case AMAZON:
case AZUL:
return "macosx";
}
default:
return platform.value();
}
}
/**
* Map Vendor and Platform to file extension
*/
public String getUrlExtension(Platform platform) {
switch(this) {
case BELLSOFT:
switch(platform) {
case LINUX:
return "tar.gz";
default:
// BellSoft uses "zip" for mac and windows platforms
return "zip";
}
default:
switch(platform) {
case WINDOWS:
return "zip";
default:
return "tar.gz";
}
}
}
public String getUrlJavaVersion(Version javaSemver) {
switch(this) {
case MICROSOFT:
case AZUL:
// Return shorted version (Microsoft, Azul suppresses the build information from URLs)
return javaSemver.toString().split("\\+")[0];
case AMAZON:
// Return lengthened version (Corretto formats major.minor.patch.build.number, e.g. 11.0.17.8.1)
String[] parts = javaSemver.toString().split("\\+");
String javaVersion = parts[0];
//
String buildAndNumber = parts[1];
if(!buildAndNumber.contains(".")) {
// Append ".1" if ".number" is missing
buildAndNumber += ".1";
}
return String.format("%s.%s", javaVersion, buildAndNumber);
}
// All others seem to prefer "+" replaced with "_"
return javaSemver.toString().replaceAll("\\+", "_");
}
}

View File

@@ -0,0 +1,328 @@
package qz.build.provision;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.codehaus.jettison.json.JSONArray;
import org.codehaus.jettison.json.JSONException;
import org.codehaus.jettison.json.JSONObject;
import qz.build.provision.params.Arch;
import qz.build.provision.params.Os;
import qz.build.provision.params.Phase;
import qz.build.provision.params.Type;
import qz.common.Constants;
import qz.installer.provision.invoker.PropertyInvoker;
import qz.utils.ArgValue;
import qz.utils.SystemUtilities;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashMap;
public class ProvisionBuilder {
protected static final Logger log = LogManager.getLogger(ProvisionBuilder.class);
public static final Path BUILD_PROVISION_FOLDER = SystemUtilities.getJarParentPath().resolve(Constants.PROVISION_DIR);
public static final File BUILD_PROVISION_FILE = BUILD_PROVISION_FOLDER.resolve(Constants.PROVISION_FILE).toFile();
private File ingestFile;
private JSONArray jsonSteps;
private Arch targetArch;
private Os targetOs;
/**
* Parses command line input to create a "provision" folder in the dist directory for customizing the installation or startup
*/
public ProvisionBuilder(String type, String phase, String os, String arch, String data, String args, String description, String ... varArgs) throws IOException, JSONException {
createProvisionDirectory(false);
targetOs = Os.ALL;
targetArch = Arch.ALL;
jsonSteps = new JSONArray();
// Wrap into JSON so that we can save it
JSONObject jsonStep = new JSONObject();
putPattern(jsonStep, "description", description);
putPattern(jsonStep, "type", type);
putPattern(jsonStep, "phase", phase);
putPattern(jsonStep, "os", os);
putPattern(jsonStep, "arch", arch);
putPattern(jsonStep, "data", data);
putPattern(jsonStep, "args", args);
putPattern(jsonStep, "arg%d", varArgs);
// Command line invocation, use the working directory
Path relativePath = Paths.get(System.getProperty("user.dir"));
ingestStep(jsonStep, relativePath);
}
/**
* To be called by ant's <code>provision</code> target
*/
public ProvisionBuilder(File antJsonFile, String antTargetOs, String antTargetArch) throws IOException, JSONException {
createProvisionDirectory(true);
// Calculate the target os, architecture
this.targetArch = Arch.parseStrict(antTargetArch);
this.targetOs = Os.parseStrict(antTargetOs);
this.jsonSteps = new JSONArray();
this.ingestFile = antJsonFile;
String jsonData = FileUtils.readFileToString(antJsonFile, StandardCharsets.UTF_8);
JSONArray pendingSteps = new JSONArray(jsonData);
// Cycle through so that each Step can be individually processed
Path relativePath = antJsonFile.toPath().getParent();
for(int i = 0; i < pendingSteps.length(); i++) {
JSONObject jsonStep = pendingSteps.getJSONObject(i);
System.out.println();
try {
ingestStep(jsonStep, relativePath);
} catch(Exception e) {
log.warn("[SKIPPED] Step '{}'", jsonStep, e);
}
}
}
public JSONArray getJson() {
return jsonSteps;
}
/**
* Construct as a Step to perform basic parsing/sanity checks
* Copy resources (if needed) to provisioning directory
*/
private void ingestStep(JSONObject jsonStep, Path relativePath) throws JSONException, IOException {
Step step = Step.parse(jsonStep, relativePath);
if(!targetOs.matches(step.os)) {
log.info("[SKIPPED] Os '{}' does not match target Os '{}' '{}'", Os.serialize(step.os), targetOs, step);
return;
}
if(!targetArch.matches(step.arch)) {
log.info("[SKIPPED] Arch '{}' does not match target Os '{}' '{}'", Arch.serialize(step.arch), targetArch, step);
return;
}
// Inject any special inferences (such as inferring resources from args)
inferAdditionalSteps(step);
if(copyResource(step)) {
log.info("[SUCCESS] Step successfully processed '{}'", step);
jsonSteps.put(step.toJSON());
// Special case for custom websocket ports
if(step.getType() == Type.PROPERTY && step.getPhase() == Phase.CERTGEN) {
HashMap<String, String> pairs = PropertyInvoker.parsePropertyPairs(step);
if(pairs.get(ArgValue.WEBSOCKET_SECURE_PORTS.getMatch()) != null ||
pairs.get(ArgValue.WEBSOCKET_INSECURE_PORTS.getMatch()) != null) {
// Clone to install step
jsonSteps.put(step.cloneTo(Phase.INSTALL).toJSON());
}
}
} else {
log.error("[SKIPPED] Resources could not be saved '{}'", step);
}
}
/**
* Save any resources files required for INSTALL and SCRIPT steps to provision folder
*/
public boolean copyResource(Step step) throws IOException {
switch(step.getType()) {
case CA:
case CERT:
case SCRIPT:
case RESOURCE:
case SOFTWARE:
boolean isRelative = !Paths.get(step.getData()).isAbsolute();
File src;
if(isRelative) {
if(ingestFile != null) {
Path parentDir = ingestFile.getParentFile().toPath();
src = parentDir.resolve(step.getData()).toFile();
} else {
throw formatted("Unable to resolve path: '%s' '%s'", step.getData(), step);
}
} else {
src = new File(step.getData());
}
String fileName = src.getName();
if(fileName.equals(BUILD_PROVISION_FILE.getName())) {
throw formatted("Resource name conflicts with provision file '%s' '%s'", fileName, step);
}
File dest = BUILD_PROVISION_FOLDER.resolve(fileName).toFile();
int i = 0;
// Avoid conflicting file names
String name = dest.getName();
// Avoid resource clobbering when being invoked by command line or providing certificates.
// Otherwise, assume the intent is to re-use the same resource (e.g. "my_script.sh", etc)
if(ingestFile == null || step.getType() == Type.CERT) {
while(dest.exists()) {
// Append "filename-1.txt" until there's no longer a conflict
if (name.contains(".")) {
dest = BUILD_PROVISION_FOLDER.resolve(String.format("%s-%s.%s", FilenameUtils.removeExtension(name), ++i,
FilenameUtils.getExtension(name))).toFile();
} else {
dest = BUILD_PROVISION_FOLDER.resolve(String.format("%-%", name, ++i)).toFile();
}
}
}
FileUtils.copyFile(src, dest);
if(dest.exists()) {
step.setData(BUILD_PROVISION_FOLDER.relativize(dest.toPath()).toString());
} else {
return false;
}
break;
default:
}
return true;
}
/**
* Appends the JSONObject to the end of the provisionFile
*/
public boolean saveJson(boolean overwrite) throws IOException, JSONException {
// Read existing JSON file if exists
JSONArray mergeSteps;
if(!overwrite && BUILD_PROVISION_FILE.exists()) {
String jsonData = FileUtils.readFileToString(BUILD_PROVISION_FILE, StandardCharsets.UTF_8);
mergeSteps = new JSONArray(jsonData);
} else {
mergeSteps = new JSONArray();
}
// Merge in new steps
for(int i = 0; i < jsonSteps.length(); i++) {
mergeSteps.put(jsonSteps.getJSONObject(i));
}
FileUtils.writeStringToFile(BUILD_PROVISION_FILE, mergeSteps.toString(3), StandardCharsets.UTF_8);
return true;
}
/**
* Convenience method for adding a name/value pair into the JSONObject
*/
private static void putPattern(JSONObject jsonStep, String name, String val) throws JSONException {
if(val != null && !val.isEmpty()) {
jsonStep.put(name, val);
}
}
/**
* Convenience method for adding consecutive patterned value pairs into the JSONObject
* e.g. --arg1 "foo" --arg2 "bar"
*/
private static void putPattern(JSONObject jsonStep, String pattern, String ... varArgs) throws JSONException {
int argCounter = 0;
for(String arg : varArgs) {
jsonStep.put(String.format(pattern, ++argCounter), arg);
}
}
private static void createProvisionDirectory(boolean cleanDirectory) throws IOException {
if(cleanDirectory) {
FileUtils.deleteDirectory(BUILD_PROVISION_FOLDER.toFile());
}
if(BUILD_PROVISION_FOLDER.toFile().isDirectory()) {
return;
}
if(BUILD_PROVISION_FOLDER.toFile().mkdirs()) {
return;
}
throw formatted("Could not create provision destination: '%'", BUILD_PROVISION_FOLDER);
}
private static IOException formatted(String message, Object ... args) {
String formatted = String.format(message, args);
return new IOException(formatted);
}
/**
* Returns the first index of the specified arg prefix pattern(s)
*
* e.g. if pattern is "/f1", it will return 1 from args { "/s", "/f1C:\foo" }
*/
private int argPrefixIndex(Step step, String ... prefixes) {
for(int i = 0; i < step.args.size() ; i++){
for(String prefix : prefixes) {
if (step.args.get(i).toLowerCase().startsWith(prefix.toLowerCase())) {
return i;
}
}
}
return -1;
}
/**
* Returns the "value" of the specified arg prefix pattern(s)
*
* e.g. if pattern is "/f1", it will return "C:\foo" from args { "/s", "/f1C:\foo" }
*
*/
private String argPrefixValue(Step step, int index, String ... prefixes) {
String arg = step.args.get(index);
String value = null;
for(String prefix : prefixes) {
if (arg.toLowerCase().startsWith(prefix.toLowerCase())) {
value = arg.substring(prefix.length());
if((value.startsWith("\"") && value.endsWith("\"")) ||
(value.startsWith("'") && value.endsWith("'"))) {
// Remove surrounding quotes
value = value.substring(1, value.length() - 1);
}
}
}
return value;
}
/**
* Clones the provided step into a new step that performs a prerequisite task.
*
* This is "magic" in the sense that it's highly specific to <code>Type</code>
* <code>Os</code> and <code>Step.args</code>.
*
* For example:
*
* Older InstallShield installers supported the <code>/f1</code> parameter which
* implies an answer file of which we need to bundle for a successful deployment.
*/
private void inferAdditionalSteps(Step orig) throws JSONException, IOException {
// Infer resource step for InstallShield .iss answer files
if(orig.getType() == Type.SOFTWARE && Os.WINDOWS.matches(orig.getOs())) {
String[] patterns = { "/f1", "-f1" };
int index = argPrefixIndex(orig, patterns);
if(index > 0) {
String resource = argPrefixValue(orig, index, patterns);
if(resource != null) {
// Clone to copy the Phase, Os and Description
Step step = orig.clone();
// Swap Type, clear args and update the data
step.setType(Type.RESOURCE);
step.setArgs(new ArrayList<>());
step.setData(resource);
if(copyResource(step)) {
File resourceFile = new File(resource);
jsonSteps.put(step.toJSON());
orig.getArgs().set(index, String.format("/f1\"%s\"", resourceFile.getName()));
log.info("[SUCCESS] Step successfully inferred and appended '{}'", step);
} else {
log.error("[SKIPPED] Resources could not be saved '{}'", step);
}
}
}
}
}
}

View File

@@ -0,0 +1,337 @@
package qz.build.provision;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.codehaus.jettison.json.JSONException;
import org.codehaus.jettison.json.JSONObject;
import qz.build.provision.params.Arch;
import qz.build.provision.params.Os;
import qz.build.provision.params.Phase;
import qz.build.provision.params.Type;
import qz.build.provision.params.types.Remover;
import qz.build.provision.params.types.Software;
import java.nio.file.Path;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
public class Step {
protected static final Logger log = LogManager.getLogger(Step.class);
String description;
Type type;
List<String> args; // Type.SCRIPT or Type.INSTALLER or Type.CONF only
HashSet<Os> os;
HashSet<Arch> arch;
Phase phase;
String data;
Path relativePath;
Class relativeClass;
public Step(Path relativePath, String description, Type type, HashSet<Os> os, HashSet<Arch> arch, Phase phase, String data, List<String> args) {
this.relativePath = relativePath;
this.description = description;
this.type = type;
this.os = os;
this.arch = arch;
this.phase = phase;
this.data = data;
this.args = args;
}
/**
* Only should be used by unit tests
*/
Step(Class relativeClass, String description, Type type, HashSet<Os> os, HashSet<Arch> arch, Phase phase, String data, List<String> args) {
this.relativeClass = relativeClass;
this.description = description;
this.type = type;
this.os = os;
this.arch = arch;
this.phase = phase;
this.data = data;
this.args = args;
}
@Override
public String toString() {
return "Step { " +
"description=\"" + description + "\", " +
"type=\"" + type + "\", " +
"os=\"" + Os.serialize(os) + "\", " +
"arch=\"" + Arch.serialize(arch) + "\", " +
"phase=\"" + phase + "\", " +
"data=\"" + data + "\", " +
"args=\"" + StringUtils.join(args, ",") + "\" " +
"}";
}
public JSONObject toJSON() throws JSONException {
JSONObject json = new JSONObject();
json.put("description", description)
.put("type", type)
.put("os", Os.serialize(os))
.put("arch", Arch.serialize(arch))
.put("phase", phase)
.put("data", data);
for(int i = 0; i < args.size(); i++) {
json.put(String.format("arg%s", i + 1), args.get(i));
}
return json;
}
public String getDescription() {
return description;
}
public Type getType() {
return type;
}
public void setType(Type type) {
this.type = type;
}
public List<String> getArgs() {
return args;
}
public void setArgs(List<String> args) {
this.args = args;
}
public HashSet<Os> getOs() {
return os;
}
public void setOs(HashSet<Os> os) {
this.os = os;
}
public HashSet<Arch> getArch() {
return arch;
}
public void setArch(HashSet<Arch> arch) {
this.arch = arch;
}
public Phase getPhase() {
return phase;
}
public void setPhase(Phase phase) {
this.phase = phase;
}
public String getData() {
return data;
}
public void setData(String data) {
this.data = data;
}
public Class getRelativeClass() {
return relativeClass;
}
public Path getRelativePath() {
return relativePath;
}
public boolean usingClass() {
return relativeClass != null;
}
public boolean usingPath() {
return relativePath != null;
}
public static Step parse(JSONObject jsonStep, Object relativeObject) {
String description = jsonStep.optString("description", "");
Type type = Type.parse(jsonStep.optString("type", null));
String data = jsonStep.optString("data", null);
// Handle installer args
List<String> args = new LinkedList<>();
if(type == Type.SOFTWARE || type == Type.CONF) {
// Handle space-delimited args
args = Software.parseArgs(jsonStep.optString("args", ""));
// Handle standalone single args (won't break on whitespace)
// e.g. "arg1": "C:\Program Files\Foo"
int argCounter = 0;
while(true) {
String singleArg = jsonStep.optString(String.format("arg%d", ++argCounter), "");
if(!singleArg.trim().isEmpty()) {
args.add(singleArg.trim());
} else {
// stop searching if the next incremental arg (e.g. "arg2") isn't found
break;
}
}
}
// Mandate "args" as the CONF path
if(type == Type.CONF) {
// Honor "path" first, if provided
String path = jsonStep.optString("path", "");
if(!path.isEmpty()) {
args.add(0, path);
}
// Keep only the first value
if(args.size() > 0) {
args = args.subList(0, 1);
} else {
throw formatted("Conf path value cannot be blank.");
}
}
HashSet<Os> os = new HashSet<>();
if(jsonStep.has("os")) {
// Do not tolerate bad os values
String osString = jsonStep.optString("os");
os = Os.parse(osString);
if(os.size() == 0) {
throw formatted("Os provided '%s' could not be parsed", osString);
}
}
HashSet<Arch> arch = new HashSet<>();
if(jsonStep.has("arch")) {
// Do not tolerate bad arch values
String archString = jsonStep.optString("arch");
arch = Arch.parse(archString);
if(arch.size() == 0) {
throw formatted("Arch provided \"%s\" could not be parsed", archString);
}
}
Phase phase = null;
if(jsonStep.has("phase")) {
String phaseString = jsonStep.optString("phase", null);
phase = Phase.parse(phaseString);
if(phase == null) {
log.warn("Phase provided \"{}\" could not be parsed", phaseString);
}
}
Step step;
if(relativeObject instanceof Path) {
step = new Step((Path)relativeObject, description, type, os, arch, phase, data, args);
} else if(relativeObject instanceof Class) {
step = new Step((Class)relativeObject, description, type, os, arch, phase, data, args);
} else {
throw formatted("Parameter relativeObject must be of type 'Path' or 'Class' but '%s' was provided", relativeObject.getClass());
}
return step.sanitize();
}
private Step sanitize() {
return throwIfNull("Type", type)
.throwIfNull("Data", data)
.validateOs()
.validateArch()
.enforcePhase(Type.PREFERENCE, Phase.STARTUP)
.enforcePhase(Type.CA, Phase.CERTGEN)
.enforcePhase(Type.CERT, Phase.STARTUP)
.enforcePhase(Type.CONF, Phase.CERTGEN)
.enforcePhase(Type.SOFTWARE, Phase.INSTALL)
.enforcePhase(Type.REMOVER, Phase.INSTALL)
.enforcePhase(Type.PROPERTY, Phase.CERTGEN, Phase.INSTALL)
.validateRemover();
}
private Step validateRemover() {
if(type != Type.REMOVER) {
return this;
}
Remover remover = Remover.parse(data);
switch(remover) {
case CUSTOM:
break;
default:
if(remover.matchesCurrentSystem()) {
throw formatted("Remover '%s' would conflict with this installer, skipping. ", remover);
}
return this;
}
// Custom removers must have three elements
if(data == null || data.split(",").length != 3) {
throw formatted("Remover data '%s' is invalid. Data must match a known type [%s] or contain exactly 3 elements.", data, Remover.valuesDelimited(","));
}
return this;
}
private Step throwIfNull(String name, Object value) {
if(value == null) {
throw formatted("%s cannot be null", name);
}
return this;
}
private Step validateOs() {
if(os == null) {
if(type == Type.SOFTWARE) {
// Software must default to a sane operating system
os = Software.parse(data).defaultOs();
} else {
os = new HashSet<>();
}
}
if(os.size() == 0) {
os.add(Os.ALL);
log.debug("Os list is null, assuming '{}'", Os.ALL);
}
return this;
}
private Step validateArch() {
if(arch == null) {
arch = new HashSet<>();
}
if(arch.size() == 0) {
arch.add(Arch.ALL);
log.debug("Arch list is null, assuming '{}'", Arch.ALL);
}
return this;
}
private Step enforcePhase(Type matchType, Phase ... requiredPhases) {
if(requiredPhases.length == 0) {
throw new UnsupportedOperationException("At least one Phase must be specified");
}
if(type == matchType) {
for(Phase requiredPhase : requiredPhases) {
if (phase == null) {
phase = requiredPhase;
log.debug("Phase is null, defaulting to '{}' based on Type '{}'", phase, type);
return this;
} else if (phase == requiredPhase) {
return this;
}
}
log.debug("Phase '{}' is unsupported for Type '{}', defaulting to '{}'", phase, type, phase = requiredPhases[0]);
}
return this;
}
private static UnsupportedOperationException formatted(String message, Object ... args) {
String formatted = String.format(message, args);
return new UnsupportedOperationException(formatted);
}
Step cloneTo(Phase phase) {
return relativePath != null ?
new Step(relativePath, description, type, os, arch, phase, data, args) :
new Step(relativeClass, description, type, os, arch, phase, data, args);
}
public Step clone() {
return cloneTo(this.phase);
}
}

View File

@@ -0,0 +1,70 @@
package qz.build.provision.params;
import org.apache.commons.lang3.StringUtils;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Locale;
/**
* Basic architecture parser
*
* Note: All aliases must be lowercase
*/
public enum Arch {
X86("x32", "i386", "i486", "i586", "i686"),
X86_64("amd64"),
ARM32("arm", "armv1", "armv2", "armv3", "armv4", "armv5", "armv6", "armv7"),
AARCH64("arm64", "armv8", "armv9"),
RISCV32("rv32"),
RISCV64("rv64"),
PPC64("powerpc", "powerpc64"),
ALL(), // special handling
UNKNOWN();
private HashSet<String> aliases = new HashSet<>();
Arch(String ... aliases) {
this.aliases.add(name().toLowerCase(Locale.ENGLISH));
this.aliases.addAll(Arrays.asList(aliases));
}
public static Arch parseStrict(String input) throws UnsupportedOperationException {
return EnumParser.parseStrict(Arch.class, input, ALL, UNKNOWN);
}
public static HashSet<Arch> parse(String input) {
return EnumParser.parseSet(Arch.class, Arch.ALL, input);
}
public static Arch parse(String input, Arch fallback) {
Arch found = bestMatch(input);
return found == UNKNOWN ? fallback : found;
}
public static Arch bestMatch(String input) {
if(input != null) {
for(Arch arch : values()) {
if (arch.aliases.contains(input.toLowerCase())) {
return arch;
}
}
}
return Arch.UNKNOWN;
}
public boolean matches(HashSet<Arch> archList) {
return this == ALL || archList.contains(ALL) || (this != UNKNOWN && archList.contains(this));
}
public static String serialize(HashSet<Arch> archList) {
if(archList.contains(ALL)) {
return "*";
}
return StringUtils.join(archList, "|");
}
@Override
public String toString() {
return super.toString().toLowerCase(Locale.ENGLISH);
}
}

View File

@@ -0,0 +1,64 @@
package qz.build.provision.params;
import java.util.EnumSet;
import java.util.HashSet;
public interface EnumParser {
/**
* Basic enum parser
*/
static <T extends Enum<T>> T parse(Class<T> clazz, String s) {
return parse(clazz, s, null);
}
static <T extends Enum<T>> T parse(Class<T> clazz, String s, T fallbackValue) {
if(s != null) {
for(T en : EnumSet.allOf(clazz)) {
if (en.name().equalsIgnoreCase(s)) {
return en;
}
}
}
return fallbackValue;
}
static <T extends Enum<T>> T parseStrict(Class<T> clazz, String s, T ... blocklist) throws UnsupportedOperationException {
if(s != null) {
HashSet<T> matched = parseSet(clazz, null, s);
if (matched.size() == 1) {
T returnVal = matched.iterator().next();
boolean blocked = false;
for(T block : blocklist) {
if(returnVal == block) {
blocked = true;
break;
}
}
if(!blocked) {
return returnVal;
}
}
}
throw new UnsupportedOperationException(String.format("%s value '%s' failed to match one and only one item", clazz.getSimpleName(), s));
}
static <T extends Enum<T>> HashSet<T> parseSet(Class<T> clazz, T all, String s) {
HashSet<T> matched = new HashSet<>();
if(s != null) {
// Handle ALL="*"
if (all != null && s.equals("*")) {
matched.add(all);
}
String[] parts = s.split("\\|");
for(String part : parts) {
T parsed = parse(clazz, part);
if (parsed != null) {
matched.add(parsed);
}
}
}
return matched;
}
}

View File

@@ -0,0 +1,67 @@
package qz.build.provision.params;
import org.apache.commons.lang3.StringUtils;
import qz.utils.SystemUtilities;
import java.util.*;
/**
* Basic OS parser
*/
public enum Os {
WINDOWS,
MAC,
LINUX,
SOLARIS, // unsupported
ALL, // special handling
UNKNOWN;
public boolean matches(HashSet<Os> osList) {
return this == ALL || osList.contains(ALL) || (this != UNKNOWN && osList.contains(this));
}
public static boolean matchesHost(HashSet<Os> osList) {
for(Os os : osList) {
if(os == SystemUtilities.getOs() || os == ALL) {
return true;
}
}
return false;
}
public static Os parseStrict(String input) throws UnsupportedOperationException {
return EnumParser.parseStrict(Os.class, input, ALL, UNKNOWN);
}
public static Os bestMatch(String input) {
if(input != null) {
String name = input.toLowerCase(Locale.ENGLISH);
if (name.contains("win")) {
return Os.WINDOWS;
} else if (name.contains("mac")) {
return Os.MAC;
} else if (name.contains("linux")) {
return Os.LINUX;
} else if (name.contains("sunos")) {
return Os.SOLARIS;
}
}
return Os.UNKNOWN;
}
public static HashSet<Os> parse(String input) {
return EnumParser.parseSet(Os.class, Os.ALL, input);
}
public static String serialize(HashSet<Os> osList) {
if(osList.contains(ALL)) {
return "*";
}
return StringUtils.join(osList, "|");
}
@Override
public String toString() {
return super.toString().toLowerCase(Locale.ENGLISH);
}
}

View File

@@ -0,0 +1,19 @@
package qz.build.provision.params;
import java.util.Locale;
public enum Phase {
INSTALL,
CERTGEN,
STARTUP,
UNINSTALL;
public static Phase parse(String input) {
return EnumParser.parse(Phase.class, input);
}
@Override
public String toString() {
return super.toString().toLowerCase(Locale.ENGLISH);
}
}

View File

@@ -0,0 +1,24 @@
package qz.build.provision.params;
import java.util.Locale;
public enum Type {
SCRIPT,
SOFTWARE,
RESOURCE,
REMOVER, // QZ Tray remover
CA,
CERT,
CONF,
PROPERTY,
PREFERENCE;
public static Type parse(String input) {
return EnumParser.parse(Type.class, input);
}
@Override
public String toString() {
return super.toString().toLowerCase(Locale.ENGLISH);
}
}

View File

@@ -0,0 +1,67 @@
package qz.build.provision.params.types;
import org.apache.commons.lang3.StringUtils;
import qz.build.provision.params.EnumParser;
import qz.common.Constants;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Locale;
public enum Remover {
QZ("QZ Tray", "qz-tray", "qz"),
CUSTOM(null, null, null); // reserved
private String aboutTitle;
private String propsFile;
private String dataDir;
Remover(String aboutTitle, String propsFile, String dataDir) {
this.aboutTitle = aboutTitle;
this.propsFile = propsFile;
this.dataDir = dataDir;
}
public String getAboutTitle() {
return aboutTitle;
}
public String getPropsFile() {
return propsFile;
}
public String getDataDir() {
return dataDir;
}
public static String valuesDelimited(String delimiter) {
ArrayList<Remover> listing = new ArrayList<>(Arrays.asList(values()));
listing.remove(CUSTOM);
return StringUtils.join(listing, delimiter).toLowerCase(Locale.ENGLISH);
}
/**
* Defaults to custom if not found
*/
public static Remover parse(String input) {
Remover remover = EnumParser.parse(Remover.class, input);
if(remover == CUSTOM) {
throw new UnsupportedOperationException("Remover 'custom' is reserved for internal purposes");
}
if(remover == null) {
remover = CUSTOM;
}
return remover;
}
public boolean matchesCurrentSystem() {
return Constants.ABOUT_TITLE.equals(aboutTitle) ||
Constants.PROPS_FILE.equals(propsFile) ||
Constants.DATA_DIR.equals(dataDir);
}
@Override
public String toString() {
return super.toString().toLowerCase(Locale.ENGLISH);
}
}

View File

@@ -0,0 +1,31 @@
package qz.build.provision.params.types;
import org.apache.commons.io.FilenameUtils;
import qz.build.provision.params.EnumParser;
import java.nio.file.Path;
import java.util.Locale;
public enum Script {
PS1,
BAT,
SH,
PY,
RB;
public static Script parse(String input) {
if(input != null && !input.isEmpty()) {
return EnumParser.parse(Script.class, FilenameUtils.getExtension(input), SH);
}
return null;
}
public static Script parse(Path path) {
return parse(path.toAbsolutePath().toString());
}
@Override
public String toString() {
return super.toString().toLowerCase(Locale.ENGLISH);
}
}

View File

@@ -0,0 +1,63 @@
package qz.build.provision.params.types;
import org.apache.commons.io.FilenameUtils;
import qz.build.provision.params.EnumParser;
import qz.build.provision.params.Os;
import java.nio.file.Path;
import java.util.*;
public enum Software {
EXE,
MSI,
PKG,
DMG,
RUN,
UNKNOWN;
public static Software parse(String input) {
return EnumParser.parse(Software.class, FilenameUtils.getExtension(input), UNKNOWN);
}
public static Software parse(Path path) {
return parse(path.toString());
}
public static List<String> parseArgs(String input) {
List<String> args = new LinkedList<>();
if(input != null) {
String[] parts = input.split(" ");
for(String part : parts) {
if(!part.trim().isEmpty()) {
args.add(part.trim());
}
}
}
return args;
}
public HashSet<Os> defaultOs() {
HashSet<Os> list = new HashSet<>();
switch(this) {
case EXE:
case MSI:
list.add(Os.WINDOWS);
break;
case PKG:
case DMG:
list.add(Os.MAC);
break;
case RUN:
list.add(Os.LINUX);
break;
default:
list.add(Os.ALL);
}
return list;
}
@Override
public String toString() {
return super.toString().toLowerCase(Locale.ENGLISH);
}
}

View File

@@ -0,0 +1,205 @@
package qz.common;
import com.github.zafarkhaja.semver.Version;
import org.apache.commons.lang3.time.DurationFormatUtils;
import org.apache.commons.ssl.Base64;
import org.bouncycastle.asn1.ASN1Primitive;
import org.bouncycastle.asn1.x509.BasicConstraints;
import org.bouncycastle.asn1.x509.Extension;
import org.bouncycastle.x509.extension.X509ExtensionUtil;
import org.codehaus.jettison.json.JSONArray;
import org.codehaus.jettison.json.JSONException;
import org.codehaus.jettison.json.JSONObject;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import qz.installer.certificate.KeyPairWrapper;
import qz.installer.certificate.CertificateManager;
import qz.utils.MacUtilities;
import qz.utils.StringUtilities;
import qz.utils.SystemUtilities;
import qz.ws.PrintSocketServer;
import qz.ws.WebsocketPorts;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.management.ManagementFactory;
import java.net.URL;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.cert.CertificateEncodingException;
import java.security.cert.X509Certificate;
import java.util.*;
public class AboutInfo {
private static final Logger log = LogManager.getLogger(AboutInfo.class);
private static String preferredHostname = "localhost";
public static JSONObject gatherAbout(String domain, CertificateManager certificateManager) {
JSONObject about = new JSONObject();
try {
about.put("product", product());
about.put("socket", socket(certificateManager, domain));
about.put("environment", environment());
about.put("ssl", ssl(certificateManager));
about.put("libraries", libraries());
about.put("charsets", charsets());
}
catch(JSONException | GeneralSecurityException e) {
log.error("Failed to write JSON data", e);
}
return about;
}
private static JSONObject product() throws JSONException {
JSONObject product = new JSONObject();
product
.put("title", Constants.ABOUT_TITLE)
.put("version", Constants.VERSION)
.put("vendor", Constants.ABOUT_COMPANY)
.put("url", Constants.ABOUT_URL);
return product;
}
private static JSONObject socket(CertificateManager certificateManager, String domain) throws JSONException {
JSONObject socket = new JSONObject();
String sanitizeDomain = StringUtilities.escapeHtmlEntities(domain);
WebsocketPorts websocketPorts = PrintSocketServer.getWebsocketPorts();
// Gracefully handle XSS per https://github.com/qzind/tray/issues/1099
if(sanitizeDomain.contains("&lt;") || sanitizeDomain.contains("&gt;")) {
log.warn("Something smells fishy about this domain: \"{}\", skipping", domain);
sanitizeDomain = "unknown";
}
socket
.put("domain", sanitizeDomain)
.put("secureProtocol", "wss")
.put("securePort", certificateManager.isSslActive() ? websocketPorts.getSecurePort() : "none")
.put("insecureProtocol", "ws")
.put("insecurePort", websocketPorts.getInsecurePort());
return socket;
}
private static JSONObject environment() throws JSONException {
JSONObject environment = new JSONObject();
long uptime = ManagementFactory.getRuntimeMXBean().getUptime();
environment
.put("os", SystemUtilities.getOsDisplayName())
.put("os version", SystemUtilities.getOsDisplayVersion())
.put("java", String.format("%s (%s)", Constants.JAVA_VERSION, SystemUtilities.getArch().toString().toLowerCase()))
.put("java (location)", System.getProperty("java.home"))
.put("java (vendor)", Constants.JAVA_VENDOR)
.put("uptime", DurationFormatUtils.formatDurationWords(uptime, true, false))
.put("uptimeMillis", uptime)
.put("sandbox", SystemUtilities.isMac() && MacUtilities.isSandboxed());
return environment;
}
private static JSONObject ssl(CertificateManager certificateManager) throws JSONException, CertificateEncodingException {
JSONObject ssl = new JSONObject();
JSONArray certs = new JSONArray();
for (KeyPairWrapper keyPair : new KeyPairWrapper[]{certificateManager.getCaKeyPair(), certificateManager.getSslKeyPair() }) {
X509Certificate x509 = keyPair.getCert();
if (x509 != null) {
JSONObject cert = new JSONObject();
cert.put("alias", keyPair.getAlias());
try {
ASN1Primitive ext = X509ExtensionUtil.fromExtensionValue(x509.getExtensionValue(Extension.basicConstraints.getId()));
cert.put("rootca", BasicConstraints.getInstance(ext).isCA());
}
catch(IOException | NullPointerException e) {
cert.put("rootca", false);
}
cert.put("subject", x509.getSubjectX500Principal().getName());
cert.put("expires", SystemUtilities.toISO(x509.getNotAfter()));
cert.put("data", formatCert(x509.getEncoded()));
certs.put(cert);
}
}
ssl.put("certificates", certs);
return ssl;
}
public static String formatCert(byte[] encoding) {
return "-----BEGIN CERTIFICATE-----\r\n" +
new String(Base64.encodeBase64(encoding, true), StandardCharsets.UTF_8) +
"-----END CERTIFICATE-----\r\n";
}
private static JSONObject libraries() throws JSONException {
JSONObject libraries = new JSONObject();
SortedMap<String,String> libs = SecurityInfo.getLibVersions();
for(Map.Entry<String,String> entry : libs.entrySet()) {
String version = entry.getValue();
if (version == null) { version = "unknown"; }
libraries.put(entry.getKey(), version);
}
return libraries;
}
private static JSONObject charsets() throws JSONException {
JSONObject charsets = new JSONObject();
SortedMap<String,Charset> avail = Charset.availableCharsets();
ArrayList<String> names = new ArrayList<>();
for(Map.Entry<String,Charset> entry : avail.entrySet()) {
names.add(entry.getValue().name());
}
charsets.put("charsets", Arrays.toString(names.toArray()));
return charsets;
}
public static String getPreferredHostname() {
return preferredHostname;
}
public static Version findLatestVersion() {
log.trace("Looking for newer versions of {} online", Constants.ABOUT_TITLE);
try {
URL api = new URL(Constants.VERSION_CHECK_URL);
BufferedReader br = new BufferedReader(new InputStreamReader(api.openStream()));
StringBuilder rawJson = new StringBuilder();
String line;
while((line = br.readLine()) != null) {
rawJson.append(line);
}
JSONArray versions = new JSONArray(rawJson.toString());
for(int i = 0; i < versions.length(); i++) {
JSONObject versionData = versions.getJSONObject(i);
if(versionData.getString("target_commitish").equals("master")) {
Version latestVersion = Version.valueOf(versionData.getString("name"));
log.trace("Found latest version of {} online: {}", Constants.ABOUT_TITLE, latestVersion);
return latestVersion;
}
}
throw new Exception("Could not find valid json version information online.");
}
catch(Exception e) {
log.error("Failed to get latest version of {} online", Constants.ABOUT_TITLE, e);
}
return Constants.VERSION;
}
}

View File

@@ -0,0 +1,156 @@
/**
* @author Antoni Ten Monro's
*
* Copyright (C) 2016 Tres Finocchiaro, QZ Industries, LLC
* Copyright (C) 2013 Antoni Ten Monro's
*
* LGPL 2.1 This is free software. This software and source code are released under
* the "LGPL 2.1 License". A copy of this license should be distributed with
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
*
*/
package qz.common;
import org.apache.commons.lang3.ArrayUtils;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
/**
* Provides a simple and efficient way for concatenating byte arrays, similar
* in purpose to <code>StringBuilder</code>. Objects of this class are not
* thread safe and include no synchronization
*
* @author Antoni Ten Monro's
*/
@SuppressWarnings("UnusedDeclaration") //Library class
public final class ByteArrayBuilder {
private List<Byte> buffer;
/**
* Creates a new <code>ByteArrayBuilder</code> and sets initial capacity to 10
*/
public ByteArrayBuilder() {
this(null);
}
/**
* Creates a new <code>ByteArrayBuilder</code> and sets initial capacity to
* <code>initialCapacity</code>
*
* @param initialCapacity the initial capacity of the <code>ByteArrayBuilder</code>
*/
public ByteArrayBuilder(int initialCapacity) {
this(null, initialCapacity);
}
/**
* Creates a new <code>ByteArrayBuilder</code>, sets initial capacity to 10
* and appends <code>initialContents</code>
*
* @param initialContents the initial contents of the ByteArrayBuilder
*/
public ByteArrayBuilder(byte[] initialContents) {
this(initialContents, 16);
}
/**
* Creates a new <code>ByteArrayBuilder</code>, sets initial capacity to
* <code>initialContents</code> and appends <code>initialContents</code>
*
* @param initialContents the initial contents of the <code>ByteArrayBuilder</code>
* @param initialCapacity the initial capacity of the <code>ByteArrayBuilder</code>
*/
public ByteArrayBuilder(byte[] initialContents, int initialCapacity) {
buffer = new ArrayList<>(initialCapacity);
if (initialContents != null) {
append(initialContents);
}
}
/**
* Empties the <code>ByteArrayBuilder</code>
*/
public void clear() {
buffer.clear();
}
/**
* Clear a portion of the <code>ByteArrayBuilder</code>
*
* @param startIndex Starting index, inclusive
* @param endIndex Ending index, exclusive
*/
public final void clearRange(int startIndex, int endIndex) {
buffer.subList(startIndex, endIndex).clear();
}
/**
* Gives the number of bytes currently stored in this <code>ByteArrayBuilder</code>
*
* @return the number of bytes in the <code>ByteArrayBuilder</code>
*/
public int getLength() {
return buffer.size();
}
/**
* Appends a new byte array to this <code>ByteArrayBuilder</code>.
* Returns this same object to allow chaining calls
*
* @param bytes the byte array to append
* @return this <code>ByteArrayBuilder</code>
*/
public final ByteArrayBuilder append(byte[] bytes) {
for(byte b : bytes) {
buffer.add(b);
}
return this;
}
public final ByteArrayBuilder append(List<Byte> bytes) {
for(byte b : bytes) {
buffer.add(b);
}
return this;
}
/**
* Convenience method for append(byte[]) combined with a StringBuffer of specified
* charset
*
* @param string the String to append
* @param charset the Charset of the String
* @return this <code>ByteArrayBuilder</code>
*/
public final ByteArrayBuilder append(String string, Charset charset) throws UnsupportedEncodingException {
return append(string.getBytes(charset.name()));
}
/**
* Convenience method for append(byte[]) combined with a String of specified
* charset
*
* @param stringBuilder the StringBuilder to append
* @param charset the Charset of the StringBuilder
* @return this <code>ByteArrayBuilder</code>
*/
public final ByteArrayBuilder append(StringBuilder stringBuilder, Charset charset) throws UnsupportedEncodingException {
return append(stringBuilder.toString(), charset);
}
/**
* Returns the full contents of this <code>ByteArrayBuilder</code> as
* a single <code>byte</code> array.
*
* @return The contents of this <code>ByteArrayBuilder</code> as a single <code>byte</code> array
*/
public byte[] getByteArray() {
return ArrayUtils.toPrimitive(buffer.toArray(new Byte[buffer.size()]));
}
}

View File

@@ -0,0 +1,97 @@
package qz.common;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;
/**
* A generic class that encapsulates an object for caching. The cached object
* will be refreshed automatically when accessed after its lifespan has expired.
*
* @param <T> The type of object to be cached.
*/
public class CachedObject<T> {
public static final long DEFAULT_LIFESPAN = 5000; // in milliseconds
T lastObject;
Supplier<T> supplier;
private long timestamp;
private long lifespan;
/**
* Creates a new CachedObject with a default lifespan of 5000 milliseconds
*
* @param supplier The function to pull new values from
*/
public CachedObject(Supplier<T> supplier) {
this(supplier, DEFAULT_LIFESPAN);
}
/**
* Creates a new CachedObject
*
* @param supplier The function to pull new values from
* @param lifespan The lifespan of the cached object in milliseconds
*/
public CachedObject(Supplier<T> supplier, long lifespan) {
this.supplier = supplier;
setLifespan(lifespan);
timestamp = Long.MIN_VALUE; // System.nanoTime() can be negative, MIN_VALUE guarantees a first-run.
}
/**
* Registers a new supplier for the CachedObject
*
* @param supplier The function to pull new values from
*/
@SuppressWarnings("unused")
public void registerSupplier(Supplier<T> supplier) {
this.supplier = supplier;
}
/**
* Sets the lifespan of the cached object
*
* @param milliseconds The lifespan of the cached object in milliseconds
*/
public void setLifespan(long milliseconds) {
lifespan = Math.max(0, milliseconds); // prevent overflow
}
/**
* Retrieves the cached object.
* If the cached object's lifespan has expired, it gets refreshed before being returned.
*
* @return The cached object
*/
public T get() {
return get(false);
}
/**
* Retrieves the cached object.
* If the cached object's lifespan is expired or forceRefresh is true, it gets refreshed before being returned.
*
* @param forceRefresh If true, the cached object will be refreshed before being returned regardless of its lifespan
* @return The cached object
*/
public T get(boolean forceRefresh) {
long now = TimeUnit.NANOSECONDS.toMillis(System.nanoTime());
// check lifespan
if (forceRefresh || (timestamp + lifespan <= now)) {
timestamp = now;
lastObject = supplier.get();
}
return lastObject;
}
// Test
public static void main(String ... args) throws InterruptedException {
final AtomicInteger testInt = new AtomicInteger(0);
CachedObject<Integer> cachedString = new CachedObject<>(testInt::incrementAndGet);
for(int i = 0; i < 100; i++) {
Thread.sleep(1500);
System.out.println(cachedString.get());
}
}
}

View File

@@ -0,0 +1,103 @@
package qz.common;
import com.github.zafarkhaja.semver.Version;
import qz.utils.SystemUtilities;
import java.awt.*;
import static qz.ws.SingleInstanceChecker.STEAL_WEBSOCKET_PROPERTY;
/**
* Created by robert on 7/9/2014.
*/
public class Constants {
public static final String HEXES = "0123456789ABCDEF";
public static final char[] HEXES_ARRAY = HEXES.toCharArray();
public static final int BYTE_BUFFER_SIZE = 8192;
public static final Version VERSION = Version.valueOf("2.2.6-SNAPSHOT");
public static final Version JAVA_VERSION = SystemUtilities.getJavaVersion();
public static final String JAVA_VENDOR = System.getProperty("java.vendor");
/* QZ-Tray Constants */
public static final String BLOCK_FILE = "blocked";
public static final String ALLOW_FILE = "allowed";
public static final String TEMP_FILE = "temp";
public static final String LOG_FILE = "debug";
public static final String PROPS_FILE = "qz-tray"; // .properties extension is assumed
public static final String PREFS_FILE = "prefs"; // .properties extension is assumed
public static final String[] PERSIST_PROPS = {"file.whitelist", "file.allow", "networking.hostname", "networking.port", STEAL_WEBSOCKET_PROPERTY };
public static final String AUTOSTART_FILE = ".autostart";
public static final String DATA_DIR = "qz";
public static final int BORDER_PADDING = 10;
public static final String ABOUT_TITLE = "QZ Tray";
public static final String ABOUT_EMAIL = "support@qz.io";
public static final String ABOUT_URL = "https://qz.io";
public static final String ABOUT_COMPANY = "QZ Industries, LLC";
public static final String ABOUT_CITY = "Canastota";
public static final String ABOUT_STATE = "NY";
public static final String ABOUT_COUNTRY = "US";
public static final String ABOUT_LICENSING_URL = Constants.ABOUT_URL + "/licensing";
public static final String ABOUT_SUPPORT_URL = Constants.ABOUT_URL + "/support";
public static final String ABOUT_PRIVACY_URL = Constants.ABOUT_URL + "/privacy";
public static final String ABOUT_DOWNLOAD_URL = Constants.ABOUT_URL + "/download";
public static final String VERSION_CHECK_URL = "https://api.github.com/repos/qzind/tray/releases";
public static final String VERSION_DOWNLOAD_URL = "https://github.com/qzind/tray/releases";
public static final boolean ENABLE_DIAGNOSTICS = true; // Diagnostics menu (logs, etc)
public static final String TRUSTED_CERT = String.format("Verified by %s", Constants.ABOUT_COMPANY);
public static final String SPONSORED_CERT = String.format("Sponsored by %s", Constants.ABOUT_COMPANY);
public static final String SPONSORED_TOOLTIP = "Sponsored organization";
public static final String UNTRUSTED_CERT = "Untrusted website";
public static final String NO_TRUST = "Cannot verify trust";
public static final String PROBE_REQUEST = "getProgramName";
public static final String PROBE_RESPONSE = ABOUT_TITLE;
public static final String ALLOW_SITES_TEXT = "Permanently allowed \"%s\" to access local resources";
public static final String BLOCK_SITES_TEXT = "Permanently blocked \"%s\" from accessing local resources";
public static final String REMEMBER_THIS_DECISION = "Remember this decision";
public static final String STRICT_MODE_LABEL = "Use strict certificate mode";
public static final String STRICT_MODE_TOOLTIP = String.format("Prevents the ability to select \"%s\" for most websites", REMEMBER_THIS_DECISION);
public static final String STRICT_MODE_CONFIRM = String.format("Set strict certificate mode? Most websites will stop working with %s.", ABOUT_TITLE);
public static final String ALLOW_SITES_LABEL = "Sites permanently allowed access";
public static final String BLOCK_SITES_LABEL = "Sites permanently blocked from access";
public static final String ALLOWED = "Allowed";
public static final String BLOCKED = "Blocked";
public static final String OVERRIDE_CERT = "override.crt";
public static final String WHITELIST_CERT_DIR = "whitelist";
public static final String PROVISION_DIR = "provision";
public static final String PROVISION_FILE = "provision.json";
public static final String SIGNING_PRIVATE_KEY = "private-key.pem";
public static final String SIGNING_CERTIFICATE = "digital-certificate.txt";
public static final long VALID_SIGNING_PERIOD = 15 * 60 * 1000; //millis
public static final int EXPIRY_WARN = 30; // days
public static final Color WARNING_COLOR_LITE = Color.RED;
public static final Color TRUSTED_COLOR_LITE = Color.BLUE;
public static final Color WARNING_COLOR_DARK = Color.decode("#EB6261");
public static final Color TRUSTED_COLOR_DARK = Color.decode("#589DF6");
public static Color WARNING_COLOR = WARNING_COLOR_LITE;
public static Color TRUSTED_COLOR = TRUSTED_COLOR_LITE;
public static boolean MASK_TRAY_SUPPORTED = true;
public static final long MEMORY_PER_PRINT = 512; //MB
public static final String RAW_PRINT = ABOUT_TITLE + " Raw Print";
public static final String IMAGE_PRINT = ABOUT_TITLE + " Pixel Print";
public static final String PDF_PRINT = ABOUT_TITLE + " PDF Print";
public static final String HTML_PRINT = ABOUT_TITLE + " HTML Print";
public static final Integer[] DEFAULT_WSS_PORTS = {8181, 8282, 8383, 8484};
public static final Integer[] DEFAULT_WS_PORTS = {8182, 8283, 8384, 8485};
public static final Integer[] CUPS_RSS_PORTS = {8586, 8687, 8788, 8889};
}

View File

@@ -0,0 +1,100 @@
package qz.common;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import qz.utils.ArgValue;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Map;
import java.util.Properties;
/**
* Created by Tres on 12/16/2015.
*/
public class PropertyHelper extends Properties {
private static final Logger log = LogManager.getLogger(PropertyHelper.class);
private String file;
/**
* Default constructor
*/
public PropertyHelper() {
super();
}
/**
* Default constructor
* @param p Initial Properties
*/
public PropertyHelper(Properties p) {
super(p);
}
/**
* Custom constructor, attempts to load from file
* @param file File to load properties from
*/
public PropertyHelper(String file) {
super();
this.file = file;
load(file);
}
public PropertyHelper(File file) {
this(file == null ? null : file.getAbsolutePath());
}
public boolean getBoolean(String key, boolean defaultVal) {
String prop = getProperty(key);
if (prop != null) {
return Boolean.parseBoolean(prop);
} else {
return defaultVal;
}
}
public void setProperty(ArgValue arg, boolean value) {
setProperty(arg.getMatch(), "" + value);
}
public void load(File file) {
load(file == null ? null : file.getAbsolutePath());
}
public void load(String file) {
FileInputStream f = null;
try {
f = new FileInputStream(file);
load(f);
} catch (IOException e) {
log.warn("Could not load file: {}, reason: {}", file, e.getLocalizedMessage());
} finally {
if (f != null) {
try { f.close(); } catch(Throwable ignore) {};
}
}
}
public boolean save() {
boolean success = false;
FileOutputStream f = null;
try {
f = new FileOutputStream(file);
this.store(f, null);
success = true;
} catch (IOException e) {
log.error("Error saving file: {}", file, e);
} finally {
if (f != null) {
try { f.close(); } catch(Throwable ignore) {};
}
}
return success;
}
public synchronized Object setProperty(Map.Entry<String, String> pair) {
return super.setProperty(pair.getKey(), pair.getValue());
}
}

View File

@@ -0,0 +1,169 @@
package qz.common;
import com.sun.jna.Native;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.eclipse.jetty.util.Jetty;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.usb4java.LibUsb;
import purejavahidapi.PureJavaHidApi;
import qz.utils.SystemUtilities;
import java.io.FileInputStream;
import java.io.IOException;
import java.lang.reflect.Method;
import java.net.URI;
import java.net.URLDecoder;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.util.*;
/**
* Created by Kyle B. on 10/27/2017.
*/
public class SecurityInfo {
/**
* Wrap throwable operations into a try/catch
*/
private static class CheckedTreeMap<K, V> extends TreeMap<K, V> {
private static final Logger log = LogManager.getLogger(CheckedTreeMap.class);
interface CheckedValue {
Object check() throws Throwable;
}
@SuppressWarnings("unchecked")
public V put(K key, CheckedValue value) {
try {
return put(key, (V)value.check());
} catch(Throwable t) {
log.warn("A checked exception was suppressed adding key \"{}\"", key, t);
return put(key, (V)"missing");
}
}
}
private static final Logger log = LogManager.getLogger(SecurityInfo.class);
public static KeyStore getKeyStore(Properties props) {
if (props != null) {
String store = props.getProperty("wss.keystore", "");
char[] pass = props.getProperty("wss.storepass", "").toCharArray();
try {
KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType());
keystore.load(new FileInputStream(store), pass);
return keystore;
}
catch(GeneralSecurityException | IOException e) {
log.warn("Unable to create keystore from properties file: {}", e.getMessage());
}
}
return null;
}
public static SortedMap<String,String> getLibVersions() {
CheckedTreeMap<String,String> libVersions = new CheckedTreeMap<>();
// Use API-provided mechanism if available
libVersions.put("jna (native)", () -> Native.VERSION_NATIVE);
libVersions.put("jna (location)", () -> {
@SuppressWarnings("unused")
int ignore = Native.BOOL_SIZE;
return System.getProperty("jnidispatch.path");
});
libVersions.put("jna", Native.VERSION);
libVersions.put("jssc", () -> jssc.SerialNativeInterface.getLibraryVersion());
libVersions.put("jssc (native)", () -> jssc.SerialNativeInterface.getNativeLibraryVersion());
libVersions.put("jetty", Jetty.VERSION);
libVersions.put("pdfbox", org.apache.pdfbox.util.Version.getVersion());
libVersions.put("purejavahidapi", () -> PureJavaHidApi.getVersion());
libVersions.put("usb-api", javax.usb.Version.getApiVersion());
libVersions.put("not-yet-commons-ssl", org.apache.commons.ssl.Version.VERSION);
libVersions.put("mslinks", mslinks.ShellLink.VERSION);
libVersions.put("bouncycastle", "" + new BouncyCastleProvider().getVersion());
libVersions.put("usb4java (native)", () -> LibUsb.getVersion().toString());
libVersions.put("jre", Constants.JAVA_VERSION.toString());
libVersions.put("jre (vendor)", Constants.JAVA_VENDOR);
//JFX info, if it exists
try {
// "DO NOT LINK JAVAFX EVER" - JavaFX may not exist, use reflection to avoid compilation errors
Class<?> VersionInfo = Class.forName("com.sun.javafx.runtime.VersionInfo");
Path fxPath = Paths.get(VersionInfo.getProtectionDomain().getCodeSource().getLocation().toURI());
Method method = VersionInfo.getMethod("getVersion");
Object version = method.invoke(null);
libVersions.put("javafx", (String)version);
libVersions.put("javafx (location)", fxPath.toString());
} catch(Throwable e) {
libVersions.put("javafx", "missing");
libVersions.put("javafx (location)", "missing");
}
// Fallback to maven manifest information
HashMap<String,String> mavenVersions = getMavenVersions();
String[] mavenLibs = {"jetty-servlet", "jetty-io", "websocket-common",
"usb4java-javax", "java-semver", "commons-pool2",
"websocket-server", "jettison", "commons-codec", "log4j-api", "log4j-core",
"websocket-servlet", "jetty-http", "commons-lang3", "javax-websocket-server-impl",
"javax.servlet-api", "hid4java", "usb4java", "websocket-api", "jetty-util", "websocket-client",
"javax.websocket-api", "commons-io", "jetty-security"};
for(String lib : mavenLibs) {
libVersions.put(lib, mavenVersions.get(lib));
}
return libVersions;
}
public static void printLibInfo() {
String format = "%-40s%s%n";
System.out.printf(format, "LIBRARY NAME:", "VERSION:");
SortedMap<String,String> libVersions = SecurityInfo.getLibVersions();
for(Map.Entry<String,String> entry : libVersions.entrySet()) {
if (entry.getValue() == null) {
System.out.printf(format, entry.getKey(), "(unknown)");
} else {
System.out.printf(format, entry.getKey(), entry.getValue());
}
}
}
/**
* Fetches embedded version information based on maven properties
*
* @return HashMap of library name, version
*/
private static HashMap<String,String> getMavenVersions() {
final HashMap<String,String> mavenVersions = new HashMap<>();
String jar = "jar:" + SecurityInfo.class.getProtectionDomain().getCodeSource().getLocation().toString();
try(FileSystem fs = FileSystems.newFileSystem(new URI(jar), new HashMap<String,String>())) {
Files.walkFileTree(fs.getPath("/META-INF/maven"), new HashSet<>(), 3, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
if (file.toString().endsWith(".properties")) {
try {
Properties props = new Properties();
props.load(Files.newInputStream(file, StandardOpenOption.READ));
mavenVersions.put(props.getProperty("artifactId"), props.getProperty("version"));
}
catch(Exception e) {
log.warn("Error reading properties from {}", file, e);
}
}
return FileVisitResult.CONTINUE;
}
});
}
catch(Exception ignore) {
log.warn("Could not open {} for version information. Most libraries will list as (unknown)", jar);
}
return mavenVersions;
}
}

View File

@@ -0,0 +1,693 @@
/**
* @author Tres Finocchiaro
*
* Copyright (C) 2016 Tres Finocchiaro, QZ Industries, LLC
*
* LGPL 2.1 This is free software. This software and source code are released under
* the "LGPL 2.1 License". A copy of this license should be distributed with
* this software. http://www.gnu.org/licenses/lgpl-2.1.html
*/
package qz.common;
import com.github.zafarkhaja.semver.Version;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.eclipse.jetty.server.Server;
import qz.App;
import qz.auth.Certificate;
import qz.auth.RequestState;
import qz.installer.shortcut.ShortcutCreator;
import qz.printer.PrintServiceMatcher;
import qz.printer.action.html.WebApp;
import qz.ui.*;
import qz.ui.component.IconCache;
import qz.ui.tray.TrayType;
import qz.utils.*;
import qz.ws.PrintSocketServer;
import qz.ws.SingleInstanceChecker;
import qz.ws.WebsocketPorts;
import qz.ws.substitutions.Substitutions;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.TimerTask;
import java.util.concurrent.TimeUnit;
import static qz.ui.component.IconCache.Icon.*;
import static qz.utils.ArgValue.*;
/**
* Manages the icons and actions associated with the TrayIcon
*
* @author Tres Finocchiaro
*/
public class TrayManager {
private static final Logger log = LogManager.getLogger(TrayManager.class);
private boolean headless;
// The cached icons
private final IconCache iconCache;
// Custom swing pop-up menu
private TrayType tray;
private ConfirmDialog confirmDialog;
private GatewayDialog gatewayDialog;
private AboutDialog aboutDialog;
private LogDialog logDialog;
private SiteManagerDialog sitesDialog;
private ArrayList<Component> componentList;
private IconCache.Icon shownIcon;
// Need a class reference to this so we can set it from the request dialog window
private JCheckBoxMenuItem anonymousItem;
// The name this UI component will use, i.e "QZ Print 1.9.0"
private final String name;
// The shortcut and startup helper
private final ShortcutCreator shortcutCreator;
private final PropertyHelper prefs;
// Action to run when reload is triggered
private Thread reloadThread;
// Actions to run if idle after startup
private java.util.Timer idleTimer = new java.util.Timer();
public TrayManager() {
this(false);
}
/**
* Create a AutoHideJSystemTray with the specified name/text
*/
public TrayManager(boolean isHeadless) {
name = Constants.ABOUT_TITLE + " " + Constants.VERSION;
prefs = new PropertyHelper(FileUtilities.USER_DIR + File.separator + Constants.PREFS_FILE + ".properties");
prefs.remove(SECURITY_FILE_STRICT.getMatch()); // per https://github.com/qzind/tray/issues/1337
// Set strict certificate mode preference
Certificate.setTrustBuiltIn(!getPref(TRAY_STRICTMODE));
// Configures JSON websocket messages
Substitutions.getInstance();
// Set FileIO security
FileUtilities.setFileIoEnabled(getPref(SECURITY_FILE_ENABLED));
FileUtilities.setFileIoStrict(getPref(SECURITY_FILE_STRICT));
// Headless if turned on by user or unsupported by environment
headless = isHeadless || getPref(HEADLESS) || GraphicsEnvironment.isHeadless();
if (headless) {
log.info("Running in headless mode");
}
// Set up the shortcut name so that the UI components can use it
shortcutCreator = ShortcutCreator.getInstance();
SystemUtilities.setSystemLookAndFeel(headless);
iconCache = new IconCache();
if (SystemUtilities.isSystemTraySupported(headless)) { // UI mode with tray
switch(SystemUtilities.getOs()) {
case WINDOWS:
tray = TrayType.JX.init(iconCache);
// Undocumented HiDPI behavior
tray.setImageAutoSize(true);
break;
case MAC:
tray = TrayType.CLASSIC.init(iconCache);
break;
default:
tray = TrayType.MODERN.init(iconCache);
}
// OS-specific tray icon handling
if (SystemTray.isSupported()) {
iconCache.fixTrayIcons(SystemUtilities.isDarkTaskbar());
}
// Iterates over all images denoted by IconCache.getTypes() and caches them
tray.setIcon(DANGER_ICON);
tray.setToolTip(name);
try {
SystemTray.getSystemTray().add(tray.tray());
}
catch(AWTException awt) {
log.error("Could not attach tray, forcing headless mode", awt);
headless = true;
}
} else if (!headless) { // UI mode without tray
tray = TrayType.TASKBAR.init(exitListener, iconCache);
tray.setIcon(DANGER_ICON);
tray.setToolTip(name);
tray.showTaskbar();
}
// TODO: Remove when fixed upstream. See issue #393
if (SystemUtilities.isUnix() && !isHeadless) {
// Update printer list in CUPS immediately (normally 2min)
System.setProperty("sun.java2d.print.polling", "false");
}
if (!headless) {
componentList = new ArrayList<>();
// The allow/block dialog
gatewayDialog = new GatewayDialog(null, "Action Required", iconCache);
componentList.add(gatewayDialog);
// The ok/cancel dialog
confirmDialog = new ConfirmDialog(null, "Please Confirm", iconCache);
componentList.add(confirmDialog);
// Detect theme changes
new Thread(() -> {
boolean darkDesktopMode = SystemUtilities.isDarkDesktop();
boolean darkTaskbarMode = SystemUtilities.isDarkTaskbar();
while(true) {
try {
Thread.sleep(1000);
if (darkDesktopMode != SystemUtilities.isDarkDesktop(true) ||
darkTaskbarMode != SystemUtilities.isDarkTaskbar(true)) {
darkDesktopMode = SystemUtilities.isDarkDesktop();
darkTaskbarMode = SystemUtilities.isDarkTaskbar();
iconCache.fixTrayIcons(darkTaskbarMode);
refreshIcon(null);
SwingUtilities.invokeLater(() -> {
SystemUtilities.setSystemLookAndFeel(headless);
for(Component c : componentList) {
SwingUtilities.updateComponentTreeUI(c);
if (c instanceof Themeable) {
((Themeable)c).refresh();
}
if (c instanceof JDialog) {
((JDialog)c).pack();
} else if (c instanceof JPopupMenu) {
((JPopupMenu)c).pack();
}
}
});
}
}
catch(InterruptedException ignore) {}
}
}).start();
}
if (tray != null) {
addMenuItems();
}
// Initialize idle actions
// Slow to start JavaFX the first time
if (getPref(TRAY_IDLE_JAVAFX)) {
performIfIdle((int)TimeUnit.SECONDS.toMillis(60), evt -> {
log.debug("IDLE: Starting up JFX for HTML printing");
try {
WebApp.initialize();
}
catch(IOException e) {
log.error("Idle runner failed to preemptively start JavaFX service");
}
});
}
// Slow to find printers the first time if a lot of printers are installed
// Must run after JavaFX per https://github.com/qzind/tray/issues/924
if (getPref(TRAY_IDLE_PRINTERS)) {
performIfIdle((int)TimeUnit.SECONDS.toMillis(120), evt -> {
log.debug("IDLE: Performing first run of find printers");
PrintServiceMatcher.getNativePrinterList(false, true);
});
}
}
/**
* Stand-alone invocation of TrayManager
*
* @param args arguments to pass to main
*/
public static void main(String args[]) {
SwingUtilities.invokeLater(TrayManager::new);
}
/**
* Builds the swing pop-up menu with the specified items
*/
private void addMenuItems() {
JPopupMenu popup = new JPopupMenu();
componentList.add(popup);
JMenu advancedMenu = new JMenu("Advanced");
advancedMenu.setMnemonic(KeyEvent.VK_A);
advancedMenu.setIcon(iconCache.getIcon(SETTINGS_ICON));
JMenuItem sitesItem = new JMenuItem("Site Manager...", iconCache.getIcon(SAVED_ICON));
sitesItem.setMnemonic(KeyEvent.VK_M);
sitesItem.addActionListener(savedListener);
sitesDialog = new SiteManagerDialog(sitesItem, iconCache, prefs);
componentList.add(sitesDialog);
JMenuItem pairingConfigItem = new JMenuItem("Pairing Configuration...", iconCache.getIcon(SETTINGS_ICON));
pairingConfigItem.setMnemonic(KeyEvent.VK_P);
pairingConfigItem.addActionListener(e -> new qz.ui.PairingConfigDialog(null).setVisible(true));
advancedMenu.add(pairingConfigItem);
JMenuItem diagnosticMenu = new JMenu("Diagnostic");
JMenuItem browseApp = new JMenuItem("Browse App folder...", iconCache.getIcon(FOLDER_ICON));
browseApp.setToolTipText(SystemUtilities.getJarParentPath().toString());
browseApp.setMnemonic(KeyEvent.VK_O);
browseApp.addActionListener(e -> ShellUtilities.browseAppDirectory());
diagnosticMenu.add(browseApp);
JMenuItem browseUser = new JMenuItem("Browse User folder...", iconCache.getIcon(FOLDER_ICON));
browseUser.setToolTipText(FileUtilities.USER_DIR.toString());
browseUser.setMnemonic(KeyEvent.VK_U);
browseUser.addActionListener(e -> ShellUtilities.browseDirectory(FileUtilities.USER_DIR));
diagnosticMenu.add(browseUser);
JMenuItem browseShared = new JMenuItem("Browse Shared folder...", iconCache.getIcon(FOLDER_ICON));
browseShared.setToolTipText(FileUtilities.SHARED_DIR.toString());
browseShared.setMnemonic(KeyEvent.VK_S);
browseShared.addActionListener(e -> ShellUtilities.browseDirectory(FileUtilities.SHARED_DIR));
diagnosticMenu.add(browseShared);
diagnosticMenu.add(new JSeparator());
JCheckBoxMenuItem notificationsItem = new JCheckBoxMenuItem("Show all notifications");
notificationsItem.setToolTipText("Shows all connect/disconnect messages, useful for debugging purposes");
notificationsItem.setMnemonic(KeyEvent.VK_S);
notificationsItem.setState(getPref(TRAY_NOTIFICATIONS));
notificationsItem.addActionListener(notificationsListener);
diagnosticMenu.add(notificationsItem);
JCheckBoxMenuItem monocleItem = new JCheckBoxMenuItem("Use Monocle for HTML");
monocleItem.setToolTipText("Use monocle platform for HTML printing (restart required)");
monocleItem.setMnemonic(KeyEvent.VK_U);
monocleItem.setState(getPref(TRAY_MONOCLE));
if(!SystemUtilities.hasMonocle()) {
log.warn("Monocle engine was not detected");
monocleItem.setEnabled(false);
monocleItem.setToolTipText("Monocle HTML engine was not detected");
}
monocleItem.addActionListener(monocleListener);
if (Constants.JAVA_VERSION.greaterThanOrEqualTo(Version.valueOf("11.0.0"))) { //only include if it can be used
diagnosticMenu.add(monocleItem);
}
diagnosticMenu.add(new JSeparator());
JMenuItem logItem = new JMenuItem("View logs (live feed)...", iconCache.getIcon(LOG_ICON));
logItem.setMnemonic(KeyEvent.VK_L);
logItem.addActionListener(logListener);
diagnosticMenu.add(logItem);
logDialog = new LogDialog(logItem, iconCache);
componentList.add(logDialog);
JMenuItem zipLogs = new JMenuItem("Zip logs (to Desktop)");
zipLogs.setToolTipText("Zip diagnostic logs, place on Desktop");
zipLogs.setMnemonic(KeyEvent.VK_Z);
zipLogs.addActionListener(e -> FileUtilities.zipLogs());
diagnosticMenu.add(zipLogs);
JMenuItem desktopItem = new JMenuItem("Create Desktop shortcut", iconCache.getIcon(DESKTOP_ICON));
desktopItem.setMnemonic(KeyEvent.VK_D);
desktopItem.addActionListener(desktopListener());
anonymousItem = new JCheckBoxMenuItem("Block anonymous requests");
anonymousItem.setToolTipText("Blocks all requests that do not contain a valid certificate/signature");
anonymousItem.setMnemonic(KeyEvent.VK_K);
anonymousItem.setState(Certificate.UNKNOWN.isBlocked());
anonymousItem.addActionListener(anonymousListener);
if(Constants.ENABLE_DIAGNOSTICS) {
advancedMenu.add(diagnosticMenu);
advancedMenu.add(new JSeparator());
}
advancedMenu.add(sitesItem);
advancedMenu.add(desktopItem);
advancedMenu.add(new JSeparator());
advancedMenu.add(anonymousItem);
JMenuItem reloadItem = new JMenuItem("Reload", iconCache.getIcon(RELOAD_ICON));
reloadItem.setMnemonic(KeyEvent.VK_R);
reloadItem.addActionListener(reloadListener);
JMenuItem aboutItem = new JMenuItem("About...", iconCache.getIcon(ABOUT_ICON));
aboutItem.setMnemonic(KeyEvent.VK_B);
aboutItem.addActionListener(aboutListener);
aboutDialog = new AboutDialog(aboutItem, iconCache);
componentList.add(aboutDialog);
if (SystemUtilities.isMac()) {
MacUtilities.registerAboutDialog(aboutDialog);
MacUtilities.registerQuitHandler(this);
}
JSeparator separator = new JSeparator();
JCheckBoxMenuItem startupItem = new JCheckBoxMenuItem("Automatically start");
startupItem.setMnemonic(KeyEvent.VK_S);
startupItem.setState(FileUtilities.isAutostart());
startupItem.addActionListener(startupListener());
if (!shortcutCreator.canAutoStart()) {
startupItem.setEnabled(false);
startupItem.setState(false);
startupItem.setToolTipText("Autostart has been disabled by the administrator");
}
JMenuItem exitItem = new JMenuItem("Exit", iconCache.getIcon(EXIT_ICON));
exitItem.addActionListener(exitListener);
popup.add(advancedMenu);
popup.add(reloadItem);
popup.add(aboutItem);
popup.add(startupItem);
popup.add(separator);
popup.add(exitItem);
if (tray != null) {
tray.setJPopupMenu(popup);
}
}
private final ActionListener notificationsListener = new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
prefs.setProperty(TRAY_NOTIFICATIONS, ((JCheckBoxMenuItem)e.getSource()).getState());
}
};
private final ActionListener monocleListener = new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
JCheckBoxMenuItem j = (JCheckBoxMenuItem)e.getSource();
prefs.setProperty(TRAY_MONOCLE, j.getState());
displayWarningMessage(String.format("A restart of %s is required to ensure this feature is %sabled.",
Constants.ABOUT_TITLE, j.getState()? "en":"dis"));
}
};
private final ActionListener desktopListener() {
return e -> {
shortcutCreator.createDesktopShortcut();
};
}
private final ActionListener savedListener = new ActionListener() {
public void actionPerformed(ActionEvent e) {
sitesDialog.setVisible(true);
}
};
private final ActionListener anonymousListener = e -> {
boolean checkBoxState = true;
if (e.getSource() instanceof JCheckBoxMenuItem) {
checkBoxState = ((JCheckBoxMenuItem)e.getSource()).getState();
}
log.debug("Block unsigned: {}", checkBoxState);
if (checkBoxState) {
blackList(Certificate.UNKNOWN);
} else {
FileUtilities.deleteFromFile(Constants.BLOCK_FILE, Certificate.UNKNOWN.data(), true);
FileUtilities.deleteFromFile(Constants.BLOCK_FILE, Certificate.UNKNOWN.data(), false);
}
};
private final ActionListener logListener = new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
logDialog.setVisible(true);
}
};
private ActionListener startupListener() {
return e -> {
JCheckBoxMenuItem source = (JCheckBoxMenuItem)e.getSource();
if (!source.getState() && !confirmDialog.prompt("Remove " + name + " from startup?")) {
source.setState(true);
return;
}
if (FileUtilities.setAutostart(source.getState())) {
displayInfoMessage("Successfully " + (source.getState() ? "enabled" : "disabled") + " autostart");
} else {
displayErrorMessage("Error " + (source.getState() ? "enabling" : "disabling") + " autostart");
}
source.setState(FileUtilities.isAutostart());
};
}
/**
* Sets the default reload action (in this case, <code>Thread.start()</code>) to be fired
*
* @param reloadThread The Thread to call when reload is clicked
*/
public void setReloadThread(Thread reloadThread) {
this.reloadThread = reloadThread;
}
private ActionListener reloadListener = new ActionListener() {
public void actionPerformed(ActionEvent e) {
if (reloadThread == null) {
showErrorDialog("Sorry, Reload has not yet been implemented.");
} else {
reloadThread.start();
}
}
};
private final ActionListener aboutListener = new ActionListener() {
public void actionPerformed(ActionEvent e) {
aboutDialog.setVisible(true);
}
};
private final ActionListener exitListener = new ActionListener() {
public void actionPerformed(ActionEvent e) {
boolean showAllNotifications = getPref(TRAY_NOTIFICATIONS);
if (!showAllNotifications || confirmDialog.prompt("Exit " + name + "?")) { exit(0); }
}
};
public void exit(int returnCode) {
prefs.save();
FileUtilities.cleanup();
System.exit(returnCode);
}
/**
* Displays a basic error dialog.
*/
private void showErrorDialog(String message) {
JOptionPane.showMessageDialog(null, message, name, JOptionPane.ERROR_MESSAGE);
}
public boolean showGatewayDialog(final RequestState request, final String prompt, final Point position) {
if (!headless) {
try {
SwingUtilities.invokeAndWait(() -> gatewayDialog.prompt("%s wants to " + prompt, request, position));
}
catch(Exception ignore) {}
if (gatewayDialog.isApproved()) {
log.info("Allowed {} to {}", request.getCertName(), prompt);
if (gatewayDialog.isPersistent()) {
whiteList(request.getCertUsed());
}
} else {
log.info("Denied {} to {}", request.getCertName(), prompt);
if (gatewayDialog.isPersistent()) {
if (!request.hasCertificate()) {
anonymousItem.doClick(); // if always block anonymous requests -> flag menu item
} else {
blackList(request.getCertUsed());
}
}
}
return gatewayDialog.isApproved();
} else {
return request.hasSavedCert();
}
}
private void whiteList(Certificate cert) {
if (FileUtilities.printLineToFile(Constants.ALLOW_FILE, cert.data())) {
displayInfoMessage(String.format(Constants.ALLOW_SITES_TEXT, cert.getOrganization()));
} else {
displayErrorMessage("Failed to write to file (Insufficient user privileges)");
}
}
private void blackList(Certificate cert) {
if (FileUtilities.printLineToFile(Constants.BLOCK_FILE, cert.data())) {
displayInfoMessage(String.format(Constants.BLOCK_SITES_TEXT, cert.getOrganization()));
} else {
displayErrorMessage("Failed to write to file (Insufficient user privileges)");
}
}
public void setServer(Server server, WebsocketPorts websocketPorts) {
if (server != null && server.getConnectors().length > 0) {
singleInstanceCheck(websocketPorts);
displayInfoMessage("Server started on port(s) " + PrintSocketServer.getPorts(server));
if (!headless) {
aboutDialog.setServer(server);
setDefaultIcon();
}
} else {
displayErrorMessage("Invalid server");
}
}
/**
* Thread safe method for setting a fine status message. Messages are suppressed unless "Show all
* notifications" is checked.
*/
public void displayInfoMessage(String text) {
displayMessage(name, text, TrayIcon.MessageType.INFO);
}
/**
* Thread safe method for setting the default icon
*/
public void setDefaultIcon() {
// Workaround for JDK-8252015
if(SystemUtilities.isMac() && Constants.MASK_TRAY_SUPPORTED && !MacUtilities.jdkSupportsTemplateIcon()) {
setIcon(DEFAULT_ICON, () -> MacUtilities.toggleTemplateIcon(tray.tray()));
} else {
setIcon(DEFAULT_ICON);
}
}
/** Thread safe method for setting the error status message */
public void displayErrorMessage(String text) {
displayMessage(name, text, TrayIcon.MessageType.ERROR);
}
/** Thread safe method for setting the danger icon */
public void setDangerIcon() {
setIcon(DANGER_ICON);
}
/** Thread safe method for setting the warning status message */
public void displayWarningMessage(String text) {
displayMessage(name, text, TrayIcon.MessageType.WARNING);
}
/** Thread safe method for setting the warning icon */
public void setWarningIcon() {
setIcon(WARNING_ICON);
}
/** Thread safe method for setting the specified icon */
private void setIcon(final IconCache.Icon i, Runnable whenDone) {
if (tray != null && i != shownIcon) {
shownIcon = i;
refreshIcon(whenDone);
}
}
private void setIcon(final IconCache.Icon i) {
setIcon(i, null);
}
public void refreshIcon(final Runnable whenDone) {
SwingUtilities.invokeLater(() -> {
tray.setIcon(shownIcon);
if(whenDone != null) {
whenDone.run();
}
});
}
/**
* Thread safe method for setting the specified status message
*
* @param caption The title of the tray message
* @param text The text body of the tray message
* @param level The message type: Level.INFO, .WARN, .SEVERE
*/
private void displayMessage(final String caption, final String text, final TrayIcon.MessageType level) {
if (!headless) {
if (tray != null) {
SwingUtilities.invokeLater(() -> {
boolean showAllNotifications = getPref(TRAY_NOTIFICATIONS);
if (showAllNotifications || level != TrayIcon.MessageType.INFO) {
tray.displayMessage(caption, text, level);
}
});
}
} else {
log.info("{}: [{}] {}", caption, level, text);
}
}
public void singleInstanceCheck(WebsocketPorts websocketPorts) {
// Secure
for(int port : websocketPorts.getUnusedSecurePorts()) {
new SingleInstanceChecker(this, port, true);
}
// Insecure
for(int port : websocketPorts.getUnusedInsecurePorts()) {
new SingleInstanceChecker(this, port, false);
}
}
public boolean isMonoclePreferred() {
return getPref(TRAY_MONOCLE);
}
public boolean isHeadless() {
return headless;
}
/**
* Get boolean user pref: Searching "user", "app" and <code>System.getProperty(...)</code>.
*/
private boolean getPref(ArgValue argValue) {
return PrefsSearch.getBoolean(argValue, prefs, App.getTrayProperties());
}
private void performIfIdle(int idleQualifier, ActionListener performer) {
if (idleTimer != null) {
idleTimer.schedule(new TimerTask() {
@Override
public void run() {
performer.actionPerformed(null);
}
}, idleQualifier);
} else {
log.warn("Idle actions have already been cleared due to activity, task not scheduled.");
}
}
public void voidIdleActions() {
if (idleTimer != null) {
log.trace("Not idle, stopping any actions that haven't ran yet");
idleTimer.cancel();
idleTimer = null;
}
}
}

View File

@@ -0,0 +1,13 @@
package qz.communication;
public class DeviceException extends Exception {
public DeviceException(String message) {
super(message);
}
public DeviceException(Throwable cause) {
super(cause);
}
}

View File

@@ -0,0 +1,29 @@
package qz.communication;
public interface DeviceIO extends DeviceListener {
String getVendorId();
String getProductId();
void open() throws DeviceException;
boolean isOpen();
void close();
void setStreaming(boolean streaming);
boolean isStreaming();
byte[] readData(int responseSize, Byte exchangeConfig) throws DeviceException;
void sendData(byte[] data, Byte exchangeConfig) throws DeviceException;
byte[] getFeatureReport(int responseSize, Byte reportId) throws DeviceException;
void sendFeatureReport(byte[] data, Byte reportId) throws DeviceException;
}

View File

@@ -0,0 +1,9 @@
package qz.communication;
public interface DeviceListener {
/**
* Cleanup task for when a socket closes while a device is still streaming
*/
void close();
}

View File

@@ -0,0 +1,130 @@
package qz.communication;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.codehaus.jettison.json.JSONObject;
import qz.utils.UsbUtilities;
public class DeviceOptions {
public enum DeviceMode {
HID,
USB,
UNKNOWN;
public static DeviceMode parse(String callName) {
if (callName != null) {
if (callName.startsWith("usb")) {
return USB;
} else if (callName.startsWith("hid")) {
return HID;
}
}
return UNKNOWN;
}
}
private DeviceMode deviceMode;
private Integer vendorId;
private Integer productId;
//usb specific
private Byte interfaceId;
private Byte endpoint;
private int interval;
private int responseSize;
//hid specific
private Integer usagePage;
private String serial;
public DeviceOptions(JSONObject parameters, DeviceMode deviceMode) {
this.deviceMode = deviceMode;
vendorId = UsbUtilities.hexToInt(parameters.optString("vendorId"));
productId = UsbUtilities.hexToInt(parameters.optString("productId"));
if (!parameters.isNull("interface")) {
interfaceId = UsbUtilities.hexToByte(parameters.optString("interface"));
}
if (!parameters.isNull("endpoint")) {
endpoint = UsbUtilities.hexToByte(parameters.optString("endpoint"));
} else if (!parameters.isNull("reportId")) {
endpoint = UsbUtilities.hexToByte(parameters.optString("reportId"));
}
interval = parameters.optInt("interval", 100);
responseSize = parameters.optInt("responseSize");
if (!parameters.isNull("usagePage")) {
usagePage = UsbUtilities.hexToInt(parameters.optString("usagePage"));
}
if (!parameters.isNull("serial")) {
serial = parameters.optString("serial", "");
serial = serial.isEmpty() ? null : serial;
}
}
public Integer getVendorId() {
return vendorId;
}
public Integer getProductId() {
return productId;
}
public Byte getInterfaceId() {
return interfaceId;
}
public Byte getEndpoint() {
return endpoint;
}
public int getInterval() {
return interval;
}
public int getResponseSize() {
return responseSize;
}
public Integer getUsagePage() {
return usagePage;
}
public String getSerial() {
return serial;
}
@Override
public boolean equals(Object obj) {
if (obj == null || !(obj instanceof DeviceOptions)) { return false; }
DeviceOptions that = (DeviceOptions)obj;
if (this.getVendorId().equals(that.getVendorId()) && this.getProductId().equals(that.getProductId())) {
if (deviceMode == DeviceMode.USB
&& (this.getInterfaceId() == null || that.getInterfaceId() == null || this.getInterfaceId().equals(that.getInterfaceId()))
&& (this.getEndpoint() == null || that.getEndpoint() == null || this.getEndpoint().equals(that.getEndpoint()))) {
return true;
}
if (deviceMode == DeviceMode.HID
&& (this.getUsagePage() == null || that.getUsagePage() == null || this.getUsagePage().equals(that.getUsagePage()))
&& (this.getSerial() == null || that.getSerial() == null || this.getSerial().equals(that.getSerial()))) {
return true;
}
}
return false;
}
@Override
public int hashCode() {
return new HashCodeBuilder()
.append(deviceMode)
.append(vendorId)
.append(productId)
.toHashCode();
}
}

View File

@@ -0,0 +1,173 @@
package qz.communication;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.IOCase;
import org.codehaus.jettison.json.JSONArray;
import org.codehaus.jettison.json.JSONException;
import org.codehaus.jettison.json.JSONObject;
import org.eclipse.jetty.websocket.api.Session;
import qz.ws.PrintSocketClient;
import qz.ws.StreamEvent;
import java.nio.channels.ClosedChannelException;
import java.nio.file.Path;
import java.nio.file.WatchKey;
import java.util.ArrayList;
public class FileIO implements DeviceListener {
public static final String SANDBOX_DATA_SUFFIX = "sandbox";
public static final String GLOBAL_DATA_SUFFIX = "shared";
// Pesky breadcrumb files that only the OS cares about
public static final String[] DEFAULT_EXCLUSIONS = { ".DS_Store", "Thumbs.db" };
public static final int FILE_LISTENER_DEFAULT_LINES = 10;
public enum ReadType {
BYTES, LINES
}
private Session session;
private Path originalPath;
private Path absolutePath;
private WatchKey wk;
private ReadType readType;
private boolean reversed;
private long bytes;
private int lines;
private IOCase caseSensitivity;
private ArrayList<String> inclusions;
private ArrayList<String> exclusions;
public FileIO(Session session, JSONObject params, Path originalPath, Path absolutePath) throws JSONException {
this.session = session;
this.originalPath = originalPath;
this.absolutePath = absolutePath;
inclusions = new ArrayList<>();
exclusions = new ArrayList<>();
JSONArray inc = params.optJSONArray("include");
JSONArray exc = params.optJSONArray("exclude");
caseSensitivity = params.optBoolean("ignoreCase", true) ? IOCase.INSENSITIVE : IOCase.SENSITIVE;
if (inc != null) {
for (int i = 0; i < inc.length(); i++) {
inclusions.add(inc.getString(i));
}
}
if (exc != null) {
for(int i = 0; i < exc.length(); i++) {
exclusions.add(exc.getString(i));
}
}
JSONObject options = params.optJSONObject("listener");
if (options != null) {
// Setup defaults
bytes = options.optLong("bytes", -1);
if (bytes > 0) {
readType = ReadType.BYTES;
} else {
readType = ReadType.LINES;
}
lines = options.optInt("lines", readType == ReadType.LINES? FILE_LISTENER_DEFAULT_LINES:-1);
reversed = options.optBoolean("reverse", readType == ReadType.LINES);
}
}
public boolean isMatch(String fileName) {
boolean match = inclusions.isEmpty();
for (String inclusion : inclusions) {
if(FilenameUtils.wildcardMatch(fileName, inclusion, caseSensitivity)) {
match = true;
break;
}
}
if(match) {
// Never match on DEFAULT_EXCLUSIONS
for(String exclusion : DEFAULT_EXCLUSIONS) {
if (FilenameUtils.wildcardMatch(fileName, exclusion, IOCase.INSENSITIVE)) {
return false;
}
}
for(String exclusion : exclusions) {
if (FilenameUtils.wildcardMatch(fileName, exclusion, caseSensitivity)) {
match = false;
break;
}
}
}
return match;
}
public boolean returnsContents() {
return bytes > 0 || lines > 0;
}
public ReadType getReadType() {
return readType;
}
public boolean isReversed() {
return reversed;
}
public long getBytes() {
return bytes;
}
public int getLines() {
return lines;
}
public Path getOriginalPath() {
return originalPath;
}
public Path getAbsolutePath() {
return absolutePath;
}
public boolean isWatching() {
return wk != null && wk.isValid();
}
public void setWk(WatchKey wk) {
this.wk = wk;
}
public void fileChanged(String fileName, String type, String fileData) throws ClosedChannelException {
StreamEvent evt = new StreamEvent(StreamEvent.Stream.FILE, StreamEvent.Type.ACTION)
.withData("file", getOriginalPath().resolve(fileName))
.withData("eventType", type);
if (fileData != null) {
evt.withData("fileData", fileData);
}
PrintSocketClient.sendStream(session, evt);
}
public void sendError(String message) throws ClosedChannelException {
StreamEvent eventErr = new StreamEvent(StreamEvent.Stream.FILE, StreamEvent.Type.ERROR)
.withData("message", message);
PrintSocketClient.sendStream(session, eventErr);
}
@Override
public void close() {
if (wk != null) {
wk.cancel();
}
}
}

View File

@@ -0,0 +1,67 @@
package qz.communication;
import org.codehaus.jettison.json.JSONException;
import org.codehaus.jettison.json.JSONObject;
import qz.utils.ByteUtilities;
import qz.utils.PrintingUtilities.Flavor;
import java.io.IOException;
import java.nio.file.OpenOption;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
/**
* Created by Kyle on 2/28/2018.
*/
public class FileParams {
private Path path;
private String data;
private Flavor flavor;
private boolean shared;
private boolean sandbox;
private OpenOption appendMode;
public FileParams(JSONObject params) throws JSONException {
path = Paths.get(params.getString("path"));
data = params.optString("data", "");
flavor = Flavor.parse(params, Flavor.PLAIN);
shared = params.optBoolean("shared", true);
sandbox = params.optBoolean("sandbox", true);
appendMode = params.optBoolean("append")? StandardOpenOption.APPEND:StandardOpenOption.TRUNCATE_EXISTING;
}
public Path getPath() {
return path;
}
public String toString(byte[] bytes) {
return ByteUtilities.toString(flavor, bytes);
}
public byte[] getData() throws IOException {
return flavor.read(data);
}
public Flavor getFlavor() {
return flavor;
}
public boolean isShared() {
return shared;
}
public boolean isSandbox() {
return sandbox;
}
public OpenOption getAppendMode() {
return appendMode;
}
}

View File

@@ -0,0 +1,110 @@
package qz.communication;
import org.hid4java.HidDevice;
import qz.ws.SocketConnection;
import javax.usb.util.UsbUtil;
public class H4J_HidIO implements DeviceIO, DeviceListener {
private HidDevice device;
private boolean streaming;
private DeviceOptions dOpts;
private SocketConnection websocket;
public H4J_HidIO(DeviceOptions dOpts, SocketConnection websocket) throws DeviceException {
this(H4J_HidUtilities.findDevice(dOpts), dOpts, websocket);
}
private H4J_HidIO(HidDevice device, DeviceOptions dOpts, SocketConnection websocket) throws DeviceException {
this.dOpts = dOpts;
this.websocket = websocket;
if (device == null) {
throw new DeviceException("HID device could not be found");
}
this.device = device;
}
public void open() {
if (!isOpen()) {
device.open();
}
}
public boolean isOpen() {
return !device.isClosed();
}
public void setStreaming(boolean active) {
streaming = active;
}
public boolean isStreaming() {
return streaming;
}
public String getVendorId() {
return UsbUtil.toHexString(device.getVendorId());
}
public String getProductId() {
return UsbUtil.toHexString(device.getProductId());
}
public byte[] readData(int responseSize, Byte unused) throws DeviceException {
byte[] response = new byte[responseSize];
int read = device.read(response);
if (read == -1) {
throw new DeviceException("Failed to read from device");
}
return response;
}
public void sendData(byte[] data, Byte reportId) throws DeviceException {
if (reportId == null) { reportId = (byte)0x00; }
int wrote = device.write(data, data.length, reportId);
if (wrote == -1) {
throw new DeviceException("Failed to write to device");
}
}
public byte[] getFeatureReport(int responseSize, Byte reportId) throws DeviceException {
if (reportId == null) { reportId = (byte)0x00; }
byte[] response = new byte[responseSize];
int read = device.getFeatureReport(response, reportId);
if (read == -1) {
throw new DeviceException("Failed to read from device");
}
return response;
}
public void sendFeatureReport(byte[] data, Byte reportId) throws DeviceException {
if (reportId == null) { reportId = (byte)0x00; }
int wrote = device.sendFeatureReport(data, reportId);
if (wrote == -1) {
throw new DeviceException("Failed to write to device");
}
}
@Override
public void close() {
setStreaming(false);
// Remove orphaned reference
websocket.removeDevice(dOpts);
if (isOpen()) {
device.close();
}
}
}

View File

@@ -0,0 +1,83 @@
package qz.communication;
import org.codehaus.jettison.json.JSONArray;
import org.eclipse.jetty.websocket.api.Session;
import org.hid4java.HidDevice;
import org.hid4java.HidManager;
import org.hid4java.HidServicesListener;
import org.hid4java.event.HidServicesEvent;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import qz.ws.PrintSocketClient;
import qz.ws.StreamEvent;
import javax.usb.util.UsbUtil;
public class H4J_HidListener implements DeviceListener, HidServicesListener {
private static final Logger log = LogManager.getLogger(H4J_HidListener.class);
private Session session;
public H4J_HidListener(Session session) {
HidManager.getHidServices().addHidServicesListener(this);
this.session = session;
}
@Override
public void hidFailure(HidServicesEvent hidServicesEvent) {
log.debug("Device failure: {}", hidServicesEvent.getHidDevice().getProduct());
PrintSocketClient.sendStream(session, createStreamAction(hidServicesEvent.getHidDevice(), "Device Failure"), this);
}
@Override
public void hidDataReceived(HidServicesEvent hidServicesEvent) {
log.debug("Data received: {}", hidServicesEvent.getDataReceived().length + " bytes");
JSONArray hex = new JSONArray();
for(byte b : hidServicesEvent.getDataReceived()) {
hex.put(UsbUtil.toHexString(b));
}
PrintSocketClient.sendStream(session, createStreamAction(hidServicesEvent.getHidDevice(), "Data Received", hex), this);
}
@Override
public void hidDeviceDetached(HidServicesEvent hidServicesEvent) {
log.debug("Device detached: {}", hidServicesEvent.getHidDevice().getProduct());
PrintSocketClient.sendStream(session, createStreamAction(hidServicesEvent.getHidDevice(), "Device Detached"), this);
}
@Override
public void hidDeviceAttached(HidServicesEvent hidServicesEvent) {
log.debug("Device attached: {}", hidServicesEvent.getHidDevice().getProduct());
PrintSocketClient.sendStream(session, createStreamAction(hidServicesEvent.getHidDevice(), "Device Attached"), this);
}
private StreamEvent createStreamAction(HidDevice device, String action) {
return createStreamAction(device, action, null);
}
private StreamEvent createStreamAction(HidDevice device, String action, JSONArray dataArr) {
StreamEvent event = new StreamEvent(StreamEvent.Stream.HID, StreamEvent.Type.ACTION)
.withData("vendorId", UsbUtil.toHexString(device.getVendorId()))
.withData("productId", UsbUtil.toHexString(device.getProductId()))
.withData("actionType", action);
if (dataArr != null) {
event.withData("data", dataArr);
}
return event;
}
@Override
public void close() {
HidManager.getHidServices().removeHidServicesListener(this);
}
}