updated control access
This commit is contained in:
@@ -83,3 +83,34 @@
|
|||||||
[2025-10-16 00:06:02 +0300] [288324] [INFO] Worker exiting (pid: 288324)
|
[2025-10-16 00:06:02 +0300] [288324] [INFO] Worker exiting (pid: 288324)
|
||||||
[2025-10-16 00:06:02 +0300] [288325] [INFO] Worker exiting (pid: 288325)
|
[2025-10-16 00:06:02 +0300] [288325] [INFO] Worker exiting (pid: 288325)
|
||||||
[2025-10-16 00:06:03 +0300] [288316] [INFO] Shutting down: Master
|
[2025-10-16 00:06:03 +0300] [288316] [INFO] Shutting down: Master
|
||||||
|
[2025-10-16 02:34:31 +0300] [299414] [INFO] Starting gunicorn 23.0.0
|
||||||
|
[2025-10-16 02:34:31 +0300] [299414] [INFO] Listening at: http://0.0.0.0:8781 (299414)
|
||||||
|
[2025-10-16 02:34:31 +0300] [299414] [INFO] Using worker: sync
|
||||||
|
[2025-10-16 02:34:31 +0300] [299414] [INFO] Trasabilitate Application server is ready. Listening on: [('0.0.0.0', 8781)]
|
||||||
|
[2025-10-16 02:34:31 +0300] [299414] [INFO] Worker spawned (pid: [booting])
|
||||||
|
[2025-10-16 02:34:31 +0300] [299432] [INFO] Booting worker with pid: 299432
|
||||||
|
[2025-10-16 02:34:31 +0300] [299432] [INFO] Worker spawned (pid: 299432)
|
||||||
|
[2025-10-16 02:34:31 +0300] [299414] [INFO] Worker spawned (pid: [booting])
|
||||||
|
[2025-10-16 02:34:31 +0300] [299438] [INFO] Booting worker with pid: 299438
|
||||||
|
[2025-10-16 02:34:31 +0300] [299438] [INFO] Worker spawned (pid: 299438)
|
||||||
|
[2025-10-16 02:34:32 +0300] [299414] [INFO] Worker spawned (pid: [booting])
|
||||||
|
[2025-10-16 02:34:32 +0300] [299439] [INFO] Booting worker with pid: 299439
|
||||||
|
[2025-10-16 02:34:32 +0300] [299439] [INFO] Worker spawned (pid: 299439)
|
||||||
|
[2025-10-16 02:34:32 +0300] [299414] [INFO] Worker spawned (pid: [booting])
|
||||||
|
[2025-10-16 02:34:32 +0300] [299440] [INFO] Booting worker with pid: 299440
|
||||||
|
[2025-10-16 02:34:32 +0300] [299440] [INFO] Worker spawned (pid: 299440)
|
||||||
|
[2025-10-16 02:34:32 +0300] [299414] [INFO] Worker spawned (pid: [booting])
|
||||||
|
[2025-10-16 02:34:32 +0300] [299441] [INFO] Booting worker with pid: 299441
|
||||||
|
[2025-10-16 02:34:32 +0300] [299441] [INFO] Worker spawned (pid: 299441)
|
||||||
|
[2025-10-16 02:34:32 +0300] [299414] [INFO] Worker spawned (pid: [booting])
|
||||||
|
[2025-10-16 02:34:32 +0300] [299442] [INFO] Booting worker with pid: 299442
|
||||||
|
[2025-10-16 02:34:32 +0300] [299442] [INFO] Worker spawned (pid: 299442)
|
||||||
|
[2025-10-16 02:34:32 +0300] [299414] [INFO] Worker spawned (pid: [booting])
|
||||||
|
[2025-10-16 02:34:32 +0300] [299443] [INFO] Booting worker with pid: 299443
|
||||||
|
[2025-10-16 02:34:32 +0300] [299443] [INFO] Worker spawned (pid: 299443)
|
||||||
|
[2025-10-16 02:34:32 +0300] [299414] [INFO] Worker spawned (pid: [booting])
|
||||||
|
[2025-10-16 02:34:32 +0300] [299444] [INFO] Booting worker with pid: 299444
|
||||||
|
[2025-10-16 02:34:32 +0300] [299444] [INFO] Worker spawned (pid: 299444)
|
||||||
|
[2025-10-16 02:34:32 +0300] [299414] [INFO] Worker spawned (pid: [booting])
|
||||||
|
[2025-10-16 02:34:32 +0300] [299445] [INFO] Booting worker with pid: 299445
|
||||||
|
[2025-10-16 02:34:32 +0300] [299445] [INFO] Worker spawned (pid: 299445)
|
||||||
|
|||||||
121
old code/migrate_external_db.py
Normal file
121
old code/migrate_external_db.py
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Script to add modules column to external database and migrate existing users
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import mariadb
|
||||||
|
|
||||||
|
def migrate_external_database():
|
||||||
|
"""Add modules column to external database and update existing users"""
|
||||||
|
try:
|
||||||
|
# Read external database configuration from instance folder
|
||||||
|
config_file = os.path.join(os.path.dirname(__file__), 'instance/external_server.conf')
|
||||||
|
if not os.path.exists(config_file):
|
||||||
|
print("External database configuration file not found at instance/external_server.conf")
|
||||||
|
return False
|
||||||
|
|
||||||
|
with open(config_file, 'r') as f:
|
||||||
|
lines = f.read().strip().split('\n')
|
||||||
|
|
||||||
|
# Parse the config file format "key=value"
|
||||||
|
config = {}
|
||||||
|
for line in lines:
|
||||||
|
if '=' in line and not line.strip().startswith('#'):
|
||||||
|
key, value = line.split('=', 1)
|
||||||
|
config[key.strip()] = value.strip()
|
||||||
|
|
||||||
|
host = config.get('server_domain', 'localhost')
|
||||||
|
port = int(config.get('port', '3306'))
|
||||||
|
database = config.get('database_name', '')
|
||||||
|
user = config.get('username', '')
|
||||||
|
password = config.get('password', '')
|
||||||
|
|
||||||
|
if not all([host, database, user, password]):
|
||||||
|
print("Missing required database configuration values.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
print(f"Connecting to external database: {host}:{port}/{database}")
|
||||||
|
|
||||||
|
# Connect to external database
|
||||||
|
conn = mariadb.connect(
|
||||||
|
user=user,
|
||||||
|
password=password,
|
||||||
|
host=host,
|
||||||
|
port=port,
|
||||||
|
database=database
|
||||||
|
)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# Check if users table exists
|
||||||
|
cursor.execute("SHOW TABLES LIKE 'users'")
|
||||||
|
if not cursor.fetchone():
|
||||||
|
print("Users table not found in external database.")
|
||||||
|
conn.close()
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Check if modules column already exists
|
||||||
|
cursor.execute("DESCRIBE users")
|
||||||
|
columns = [row[0] for row in cursor.fetchall()]
|
||||||
|
|
||||||
|
if 'modules' not in columns:
|
||||||
|
print("Adding modules column to users table...")
|
||||||
|
cursor.execute("ALTER TABLE users ADD COLUMN modules TEXT")
|
||||||
|
print("Modules column added successfully.")
|
||||||
|
else:
|
||||||
|
print("Modules column already exists.")
|
||||||
|
|
||||||
|
# Get current users and convert their roles
|
||||||
|
cursor.execute("SELECT id, username, role FROM users")
|
||||||
|
users = cursor.fetchall()
|
||||||
|
|
||||||
|
role_mapping = {
|
||||||
|
'superadmin': ('superadmin', None),
|
||||||
|
'administrator': ('admin', None),
|
||||||
|
'admin': ('admin', None),
|
||||||
|
'quality': ('manager', '["quality"]'),
|
||||||
|
'warehouse': ('manager', '["warehouse"]'),
|
||||||
|
'warehouse_manager': ('manager', '["warehouse"]'),
|
||||||
|
'scan': ('worker', '["quality"]'),
|
||||||
|
'etichete': ('manager', '["labels"]'),
|
||||||
|
'quality_manager': ('manager', '["quality"]'),
|
||||||
|
'quality_worker': ('worker', '["quality"]'),
|
||||||
|
}
|
||||||
|
|
||||||
|
print(f"Migrating {len(users)} users...")
|
||||||
|
|
||||||
|
for user_id, username, old_role in users:
|
||||||
|
if old_role in role_mapping:
|
||||||
|
new_role, modules_json = role_mapping[old_role]
|
||||||
|
|
||||||
|
cursor.execute("UPDATE users SET role = ?, modules = ? WHERE id = ?",
|
||||||
|
(new_role, modules_json, user_id))
|
||||||
|
|
||||||
|
print(f" {username}: {old_role} -> {new_role} with modules {modules_json}")
|
||||||
|
else:
|
||||||
|
print(f" {username}: Unknown role '{old_role}', keeping as-is")
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
print("External database migration completed successfully!")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error migrating external database: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
print("External Database Migration for Simplified 4-Tier Permission System")
|
||||||
|
print("=" * 70)
|
||||||
|
|
||||||
|
success = migrate_external_database()
|
||||||
|
|
||||||
|
if success:
|
||||||
|
print("\n✅ Migration completed successfully!")
|
||||||
|
print("\nUsers can now log in with the new simplified permission system.")
|
||||||
|
print("Role structure: superadmin → admin → manager → worker")
|
||||||
|
print("Modules: quality, warehouse, labels")
|
||||||
|
else:
|
||||||
|
print("\n❌ Migration failed. Please check the error messages above.")
|
||||||
172
old code/migrate_permissions.py
Executable file
172
old code/migrate_permissions.py
Executable file
@@ -0,0 +1,172 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Migration script to convert from complex permission system to simplified 4-tier system
|
||||||
|
This script will:
|
||||||
|
1. Add 'modules' column to users table
|
||||||
|
2. Convert existing roles to new 4-tier system
|
||||||
|
3. Assign appropriate modules based on old roles
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sqlite3
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Add the app directory to Python path
|
||||||
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
|
||||||
|
|
||||||
|
def get_db_connections():
|
||||||
|
"""Get both internal SQLite and external database connections"""
|
||||||
|
connections = {}
|
||||||
|
|
||||||
|
# Internal SQLite database
|
||||||
|
internal_db_path = os.path.join(os.path.dirname(__file__), 'instance/users.db')
|
||||||
|
if os.path.exists(internal_db_path):
|
||||||
|
connections['internal'] = sqlite3.connect(internal_db_path)
|
||||||
|
print(f"Connected to internal SQLite database: {internal_db_path}")
|
||||||
|
|
||||||
|
# External database (try to connect using existing method)
|
||||||
|
try:
|
||||||
|
import mariadb
|
||||||
|
|
||||||
|
# Read external database configuration
|
||||||
|
config_file = os.path.join(os.path.dirname(__file__), '../external_database_settings')
|
||||||
|
if os.path.exists(config_file):
|
||||||
|
with open(config_file, 'r') as f:
|
||||||
|
lines = f.read().strip().split('\n')
|
||||||
|
if len(lines) >= 5:
|
||||||
|
host = lines[0].strip()
|
||||||
|
port = int(lines[1].strip())
|
||||||
|
database = lines[2].strip()
|
||||||
|
user = lines[3].strip()
|
||||||
|
password = lines[4].strip()
|
||||||
|
|
||||||
|
conn = mariadb.connect(
|
||||||
|
user=user,
|
||||||
|
password=password,
|
||||||
|
host=host,
|
||||||
|
port=port,
|
||||||
|
database=database
|
||||||
|
)
|
||||||
|
connections['external'] = conn
|
||||||
|
print(f"Connected to external MariaDB database: {host}:{port}/{database}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Could not connect to external database: {e}")
|
||||||
|
|
||||||
|
return connections
|
||||||
|
|
||||||
|
def role_mapping():
|
||||||
|
"""Map old roles to new 4-tier system"""
|
||||||
|
return {
|
||||||
|
# Old role -> (new_role, modules)
|
||||||
|
'superadmin': ('superadmin', []), # All modules by default
|
||||||
|
'administrator': ('admin', []), # All modules by default
|
||||||
|
'admin': ('admin', []), # All modules by default
|
||||||
|
'quality': ('manager', ['quality']),
|
||||||
|
'warehouse': ('manager', ['warehouse']),
|
||||||
|
'warehouse_manager': ('manager', ['warehouse']),
|
||||||
|
'scan': ('worker', ['quality']), # Assume scan users are quality workers
|
||||||
|
'etichete': ('manager', ['labels']),
|
||||||
|
'quality_manager': ('manager', ['quality']),
|
||||||
|
'quality_worker': ('worker', ['quality']),
|
||||||
|
}
|
||||||
|
|
||||||
|
def migrate_database(conn, db_type):
|
||||||
|
"""Migrate a specific database"""
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
print(f"Migrating {db_type} database...")
|
||||||
|
|
||||||
|
# Check if users table exists
|
||||||
|
if db_type == 'internal':
|
||||||
|
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='users'")
|
||||||
|
else: # external/MariaDB
|
||||||
|
cursor.execute("SHOW TABLES LIKE 'users'")
|
||||||
|
|
||||||
|
if not cursor.fetchone():
|
||||||
|
print(f"No users table found in {db_type} database")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Check if modules column already exists
|
||||||
|
try:
|
||||||
|
if db_type == 'internal':
|
||||||
|
cursor.execute("PRAGMA table_info(users)")
|
||||||
|
columns = [row[1] for row in cursor.fetchall()]
|
||||||
|
else: # external/MariaDB
|
||||||
|
cursor.execute("DESCRIBE users")
|
||||||
|
columns = [row[0] for row in cursor.fetchall()]
|
||||||
|
|
||||||
|
if 'modules' not in columns:
|
||||||
|
print(f"Adding modules column to {db_type} database...")
|
||||||
|
if db_type == 'internal':
|
||||||
|
cursor.execute("ALTER TABLE users ADD COLUMN modules TEXT")
|
||||||
|
else: # external/MariaDB
|
||||||
|
cursor.execute("ALTER TABLE users ADD COLUMN modules TEXT")
|
||||||
|
else:
|
||||||
|
print(f"Modules column already exists in {db_type} database")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error checking/adding modules column in {db_type}: {e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Get current users
|
||||||
|
cursor.execute("SELECT id, username, role FROM users")
|
||||||
|
users = cursor.fetchall()
|
||||||
|
|
||||||
|
print(f"Found {len(users)} users in {db_type} database")
|
||||||
|
|
||||||
|
# Convert roles and assign modules
|
||||||
|
mapping = role_mapping()
|
||||||
|
updates = []
|
||||||
|
|
||||||
|
for user_id, username, old_role in users:
|
||||||
|
if old_role in mapping:
|
||||||
|
new_role, modules = mapping[old_role]
|
||||||
|
modules_json = json.dumps(modules) if modules else None
|
||||||
|
updates.append((new_role, modules_json, user_id, username))
|
||||||
|
print(f" {username}: {old_role} -> {new_role} with modules {modules}")
|
||||||
|
else:
|
||||||
|
print(f" {username}: Unknown role '{old_role}', keeping as-is")
|
||||||
|
|
||||||
|
# Apply updates
|
||||||
|
for new_role, modules_json, user_id, username in updates:
|
||||||
|
try:
|
||||||
|
cursor.execute("UPDATE users SET role = ?, modules = ? WHERE id = ?",
|
||||||
|
(new_role, modules_json, user_id))
|
||||||
|
print(f" Updated {username} successfully")
|
||||||
|
except Exception as e:
|
||||||
|
print(f" Error updating {username}: {e}")
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
print(f"Migration completed for {db_type} database")
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Main migration function"""
|
||||||
|
print("Starting migration to simplified 4-tier permission system...")
|
||||||
|
print("="*60)
|
||||||
|
|
||||||
|
connections = get_db_connections()
|
||||||
|
|
||||||
|
if not connections:
|
||||||
|
print("No database connections available. Please check your configuration.")
|
||||||
|
return
|
||||||
|
|
||||||
|
for db_type, conn in connections.items():
|
||||||
|
try:
|
||||||
|
migrate_database(conn, db_type)
|
||||||
|
print()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error migrating {db_type} database: {e}")
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
print("Migration completed!")
|
||||||
|
print("\nNew role structure:")
|
||||||
|
print("- superadmin: Full system access")
|
||||||
|
print("- admin: Full app access (except role_permissions and download_extension)")
|
||||||
|
print("- manager: Module-based access (can have multiple modules)")
|
||||||
|
print("- worker: Limited module access (one module only)")
|
||||||
|
print("\nAvailable modules: quality, warehouse, labels")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
111
old code/test_permissions.py
Normal file
111
old code/test_permissions.py
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Test script for the new simplified 4-tier permission system
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'app'))
|
||||||
|
|
||||||
|
from permissions_simple import check_access, validate_user_modules, get_user_accessible_pages
|
||||||
|
|
||||||
|
def test_permission_system():
|
||||||
|
"""Test the new permission system with various scenarios"""
|
||||||
|
print("Testing Simplified 4-Tier Permission System")
|
||||||
|
print("=" * 50)
|
||||||
|
|
||||||
|
# Test cases: (role, modules, page, expected_result)
|
||||||
|
test_cases = [
|
||||||
|
# Superadmin tests
|
||||||
|
('superadmin', [], 'dashboard', True),
|
||||||
|
('superadmin', [], 'role_permissions', True),
|
||||||
|
('superadmin', [], 'quality', True),
|
||||||
|
('superadmin', [], 'warehouse', True),
|
||||||
|
|
||||||
|
# Admin tests
|
||||||
|
('admin', [], 'dashboard', True),
|
||||||
|
('admin', [], 'role_permissions', False), # Restricted for admin
|
||||||
|
('admin', [], 'download_extension', False), # Restricted for admin
|
||||||
|
('admin', [], 'quality', True),
|
||||||
|
('admin', [], 'warehouse', True),
|
||||||
|
|
||||||
|
# Manager tests
|
||||||
|
('manager', ['quality'], 'quality', True),
|
||||||
|
('manager', ['quality'], 'quality_reports', True),
|
||||||
|
('manager', ['quality'], 'warehouse', False), # No warehouse module
|
||||||
|
('manager', ['warehouse'], 'warehouse', True),
|
||||||
|
('manager', ['warehouse'], 'quality', False), # No quality module
|
||||||
|
('manager', ['quality', 'warehouse'], 'quality', True), # Multiple modules
|
||||||
|
('manager', ['quality', 'warehouse'], 'warehouse', True),
|
||||||
|
|
||||||
|
# Worker tests
|
||||||
|
('worker', ['quality'], 'quality', True),
|
||||||
|
('worker', ['quality'], 'quality_reports', False), # Workers can't access reports
|
||||||
|
('worker', ['quality'], 'warehouse', False), # No warehouse module
|
||||||
|
('worker', ['warehouse'], 'move_orders', True),
|
||||||
|
('worker', ['warehouse'], 'create_locations', False), # Workers can't create locations
|
||||||
|
|
||||||
|
# Invalid role test
|
||||||
|
('invalid_role', ['quality'], 'quality', False),
|
||||||
|
]
|
||||||
|
|
||||||
|
print("Testing access control:")
|
||||||
|
print("-" * 30)
|
||||||
|
|
||||||
|
passed = 0
|
||||||
|
failed = 0
|
||||||
|
|
||||||
|
for role, modules, page, expected in test_cases:
|
||||||
|
result = check_access(role, modules, page)
|
||||||
|
status = "PASS" if result == expected else "FAIL"
|
||||||
|
print(f"{status}: {role:12} {str(modules):20} {page:18} -> {result} (expected {expected})")
|
||||||
|
|
||||||
|
if result == expected:
|
||||||
|
passed += 1
|
||||||
|
else:
|
||||||
|
failed += 1
|
||||||
|
|
||||||
|
print(f"\nResults: {passed} passed, {failed} failed")
|
||||||
|
|
||||||
|
# Test module validation
|
||||||
|
print("\nTesting module validation:")
|
||||||
|
print("-" * 30)
|
||||||
|
|
||||||
|
validation_tests = [
|
||||||
|
('superadmin', ['quality'], True), # Superadmin can have any modules
|
||||||
|
('admin', ['warehouse'], True), # Admin can have any modules
|
||||||
|
('manager', ['quality'], True), # Manager can have one module
|
||||||
|
('manager', ['quality', 'warehouse'], True), # Manager can have multiple modules
|
||||||
|
('manager', [], False), # Manager must have at least one module
|
||||||
|
('worker', ['quality'], True), # Worker can have one module
|
||||||
|
('worker', ['quality', 'warehouse'], False), # Worker cannot have multiple modules
|
||||||
|
('worker', [], False), # Worker must have exactly one module
|
||||||
|
('invalid_role', ['quality'], False), # Invalid role
|
||||||
|
]
|
||||||
|
|
||||||
|
for role, modules, expected in validation_tests:
|
||||||
|
is_valid, error_msg = validate_user_modules(role, modules)
|
||||||
|
status = "PASS" if is_valid == expected else "FAIL"
|
||||||
|
print(f"{status}: {role:12} {str(modules):20} -> {is_valid} (expected {expected})")
|
||||||
|
if error_msg:
|
||||||
|
print(f" Error: {error_msg}")
|
||||||
|
|
||||||
|
# Test accessible pages for different users
|
||||||
|
print("\nTesting accessible pages:")
|
||||||
|
print("-" * 30)
|
||||||
|
|
||||||
|
user_tests = [
|
||||||
|
('superadmin', []),
|
||||||
|
('admin', []),
|
||||||
|
('manager', ['quality']),
|
||||||
|
('manager', ['warehouse']),
|
||||||
|
('worker', ['quality']),
|
||||||
|
('worker', ['warehouse']),
|
||||||
|
]
|
||||||
|
|
||||||
|
for role, modules in user_tests:
|
||||||
|
accessible_pages = get_user_accessible_pages(role, modules)
|
||||||
|
print(f"{role:12} {str(modules):20} -> {len(accessible_pages)} pages: {', '.join(accessible_pages[:5])}{'...' if len(accessible_pages) > 5 else ''}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
test_permission_system()
|
||||||
65
old code/tray/src/qz/communication/H4J_HidUtilities.java
Executable file
65
old code/tray/src/qz/communication/H4J_HidUtilities.java
Executable file
@@ -0,0 +1,65 @@
|
|||||||
|
package qz.communication;
|
||||||
|
|
||||||
|
|
||||||
|
import org.codehaus.jettison.json.JSONArray;
|
||||||
|
import org.codehaus.jettison.json.JSONException;
|
||||||
|
import org.codehaus.jettison.json.JSONObject;
|
||||||
|
import org.hid4java.HidDevice;
|
||||||
|
import org.hid4java.HidManager;
|
||||||
|
import org.hid4java.HidServices;
|
||||||
|
|
||||||
|
import javax.usb.util.UsbUtil;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class H4J_HidUtilities {
|
||||||
|
private static final HidServices service = HidManager.getHidServices();
|
||||||
|
|
||||||
|
public static List<HidDevice> getHidDevices() {
|
||||||
|
return service.getAttachedHidDevices();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static JSONArray getHidDevicesJSON() throws JSONException {
|
||||||
|
List<HidDevice> devices = getHidDevices();
|
||||||
|
JSONArray devicesJSON = new JSONArray();
|
||||||
|
|
||||||
|
HashSet<String> unique = new HashSet<>();
|
||||||
|
for(HidDevice device : devices) {
|
||||||
|
JSONObject deviceJSON = new JSONObject();
|
||||||
|
|
||||||
|
deviceJSON.put("vendorId", UsbUtil.toHexString(device.getVendorId()))
|
||||||
|
.put("productId", UsbUtil.toHexString(device.getProductId()))
|
||||||
|
.put("usagePage", UsbUtil.toHexString((short)device.getUsagePage()))
|
||||||
|
.put("serial", device.getSerialNumber())
|
||||||
|
.put("manufacturer", device.getManufacturer())
|
||||||
|
.put("product", device.getProduct());
|
||||||
|
|
||||||
|
String uid = String.format("v%sp%su%ss%s", deviceJSON.optString("vendorId"), deviceJSON.optString("productId"), deviceJSON.optString("usagePage"), deviceJSON.optString("serial"));
|
||||||
|
if (!unique.contains(uid)) {
|
||||||
|
devicesJSON.put(deviceJSON);
|
||||||
|
unique.add(uid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return devicesJSON;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static HidDevice findDevice(DeviceOptions dOpts) {
|
||||||
|
if (dOpts.getVendorId() == null) {
|
||||||
|
throw new IllegalArgumentException("Vendor ID cannot be null");
|
||||||
|
}
|
||||||
|
if (dOpts.getProductId() == null) {
|
||||||
|
throw new IllegalArgumentException("Product ID cannot be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
List<HidDevice> devices = getHidDevices();
|
||||||
|
for(HidDevice device : devices) {
|
||||||
|
if (device.isVidPidSerial(dOpts.getVendorId(), dOpts.getProductId(), dOpts.getSerial())
|
||||||
|
&& (dOpts.getUsagePage() == null || dOpts.getUsagePage() == device.getUsagePage())) {
|
||||||
|
return device;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
154
old code/tray/src/qz/communication/PJHA_HidIO.java
Executable file
154
old code/tray/src/qz/communication/PJHA_HidIO.java
Executable file
@@ -0,0 +1,154 @@
|
|||||||
|
package qz.communication;
|
||||||
|
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
import purejavahidapi.HidDevice;
|
||||||
|
import purejavahidapi.HidDeviceInfo;
|
||||||
|
import purejavahidapi.InputReportListener;
|
||||||
|
import purejavahidapi.PureJavaHidApi;
|
||||||
|
import qz.utils.SystemUtilities;
|
||||||
|
import qz.ws.SocketConnection;
|
||||||
|
|
||||||
|
import javax.usb.util.UsbUtil;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Vector;
|
||||||
|
|
||||||
|
public class PJHA_HidIO implements DeviceIO {
|
||||||
|
|
||||||
|
private static final Logger log = LogManager.getLogger(PJHA_HidIO.class);
|
||||||
|
|
||||||
|
private HidDeviceInfo deviceInfo;
|
||||||
|
private HidDevice device;
|
||||||
|
|
||||||
|
private static final int BUFFER_SIZE = 32;
|
||||||
|
private Vector<byte[]> dataBuffer;
|
||||||
|
private boolean streaming;
|
||||||
|
private DeviceOptions dOpts;
|
||||||
|
private SocketConnection websocket;
|
||||||
|
|
||||||
|
public PJHA_HidIO(DeviceOptions dOpts, SocketConnection websocket) throws DeviceException {
|
||||||
|
this(PJHA_HidUtilities.findDevice(dOpts), dOpts, websocket);
|
||||||
|
}
|
||||||
|
|
||||||
|
private PJHA_HidIO(HidDeviceInfo deviceInfo, DeviceOptions dOpts, SocketConnection websocket) throws DeviceException {
|
||||||
|
this.dOpts = dOpts;
|
||||||
|
this.websocket = websocket;
|
||||||
|
if (deviceInfo == null) {
|
||||||
|
throw new DeviceException("HID device could not be found");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.deviceInfo = deviceInfo;
|
||||||
|
|
||||||
|
dataBuffer = new Vector<byte[]>() {
|
||||||
|
@Override
|
||||||
|
public synchronized boolean add(byte[] e) {
|
||||||
|
while(this.size() >= BUFFER_SIZE) {
|
||||||
|
this.remove(0);
|
||||||
|
}
|
||||||
|
return super.add(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public void open() throws DeviceException {
|
||||||
|
if (!isOpen()) {
|
||||||
|
try {
|
||||||
|
device = PureJavaHidApi.openDevice(deviceInfo);
|
||||||
|
device.setInputReportListener(new InputReportListener() {
|
||||||
|
@Override
|
||||||
|
public void onInputReport(HidDevice source, byte id, byte[] data, int len) {
|
||||||
|
byte[] dataCopy = new byte[len];
|
||||||
|
System.arraycopy(data, 0, dataCopy, 0, len);
|
||||||
|
dataBuffer.add(dataCopy);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch(IOException ex) {
|
||||||
|
throw new DeviceException(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isOpen() {
|
||||||
|
return device != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStreaming(boolean active) {
|
||||||
|
streaming = active;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isStreaming() {
|
||||||
|
return streaming;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getVendorId() {
|
||||||
|
return UsbUtil.toHexString(deviceInfo.getVendorId());
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getProductId() {
|
||||||
|
return UsbUtil.toHexString(deviceInfo.getProductId());
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] readData(int responseSize, Byte unused) throws DeviceException {
|
||||||
|
byte[] response = new byte[responseSize];
|
||||||
|
if (dataBuffer.isEmpty()) {
|
||||||
|
return new byte[0]; //no data received yet
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] latestData = dataBuffer.remove(0);
|
||||||
|
if (SystemUtilities.isWindows()) {
|
||||||
|
//windows missing the leading byte
|
||||||
|
System.arraycopy(latestData, 0, response, 1, Math.min(responseSize - 1, latestData.length));
|
||||||
|
} else {
|
||||||
|
System.arraycopy(latestData, 0, response, 0, Math.min(responseSize - 1, latestData.length));
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendData(byte[] data, Byte reportId) throws DeviceException {
|
||||||
|
if (reportId == null) { reportId = (byte)0x00; }
|
||||||
|
|
||||||
|
int wrote = device.setOutputReport(reportId, data, data.length);
|
||||||
|
if (wrote == -1) {
|
||||||
|
throw new DeviceException("Failed to write to device");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getFeatureReport(int responseSize, Byte unused) throws DeviceException {
|
||||||
|
byte[] response = new byte[responseSize];
|
||||||
|
int read = device.getFeatureReport(response, responseSize);
|
||||||
|
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.setFeatureReport(reportId, data, data.length);
|
||||||
|
|
||||||
|
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()) {
|
||||||
|
try {
|
||||||
|
device.setInputReportListener(null);
|
||||||
|
device.close();
|
||||||
|
}
|
||||||
|
catch(IllegalStateException e) {
|
||||||
|
log.warn("Device already closed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
device = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
50
old code/tray/src/qz/communication/PJHA_HidListener.java
Executable file
50
old code/tray/src/qz/communication/PJHA_HidListener.java
Executable file
@@ -0,0 +1,50 @@
|
|||||||
|
package qz.communication;
|
||||||
|
|
||||||
|
import org.eclipse.jetty.websocket.api.Session;
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
import purejavahidapi.DeviceRemovalListener;
|
||||||
|
import purejavahidapi.HidDevice;
|
||||||
|
import qz.ws.PrintSocketClient;
|
||||||
|
import qz.ws.StreamEvent;
|
||||||
|
|
||||||
|
import javax.usb.util.UsbUtil;
|
||||||
|
|
||||||
|
public class PJHA_HidListener implements DeviceListener, DeviceRemovalListener {
|
||||||
|
|
||||||
|
private static final Logger log = LogManager.getLogger(PJHA_HidListener.class);
|
||||||
|
|
||||||
|
private Session session;
|
||||||
|
private HidDevice device;
|
||||||
|
|
||||||
|
|
||||||
|
public PJHA_HidListener(Session session) {
|
||||||
|
this.session = session;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDevice(HidDevice device) {
|
||||||
|
this.device = device;
|
||||||
|
device.setDeviceRemovalListener(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private StreamEvent createStreamAction(HidDevice device, String action) {
|
||||||
|
return new StreamEvent(StreamEvent.Stream.HID, StreamEvent.Type.ACTION)
|
||||||
|
.withData("vendorId", UsbUtil.toHexString(device.getHidDeviceInfo().getVendorId()))
|
||||||
|
.withData("productId", UsbUtil.toHexString(device.getHidDeviceInfo().getProductId()))
|
||||||
|
.withData("actionType", action);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
if (device != null) {
|
||||||
|
device.setDeviceRemovalListener(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDeviceRemoval(HidDevice device) {
|
||||||
|
log.debug("Device detached: {}", device.getHidDeviceInfo().getProductString());
|
||||||
|
PrintSocketClient.sendStream(session, createStreamAction(device, "Device Detached"), this);
|
||||||
|
}
|
||||||
|
}
|
||||||
56
old code/tray/src/qz/communication/PJHA_HidUtilities.java
Executable file
56
old code/tray/src/qz/communication/PJHA_HidUtilities.java
Executable file
@@ -0,0 +1,56 @@
|
|||||||
|
package qz.communication;
|
||||||
|
|
||||||
|
|
||||||
|
import org.codehaus.jettison.json.JSONArray;
|
||||||
|
import org.codehaus.jettison.json.JSONException;
|
||||||
|
import org.codehaus.jettison.json.JSONObject;
|
||||||
|
import purejavahidapi.HidDeviceInfo;
|
||||||
|
import purejavahidapi.PureJavaHidApi;
|
||||||
|
|
||||||
|
import javax.usb.util.UsbUtil;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class PJHA_HidUtilities {
|
||||||
|
|
||||||
|
public static JSONArray getHidDevicesJSON() throws JSONException {
|
||||||
|
List<HidDeviceInfo> devices = PureJavaHidApi.enumerateDevices();
|
||||||
|
JSONArray devicesJSON = new JSONArray();
|
||||||
|
|
||||||
|
for(HidDeviceInfo device : devices) {
|
||||||
|
JSONObject deviceJSON = new JSONObject();
|
||||||
|
|
||||||
|
deviceJSON.put("vendorId", UsbUtil.toHexString(device.getVendorId()))
|
||||||
|
.put("productId", UsbUtil.toHexString(device.getProductId()))
|
||||||
|
.put("usagePage", UsbUtil.toHexString(device.getUsagePage()))
|
||||||
|
.put("serial", device.getSerialNumberString())
|
||||||
|
.put("manufacturer", device.getManufacturerString())
|
||||||
|
.put("product", device.getProductString());
|
||||||
|
|
||||||
|
devicesJSON.put(deviceJSON);
|
||||||
|
}
|
||||||
|
|
||||||
|
return devicesJSON;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static HidDeviceInfo findDevice(DeviceOptions dOpts) {
|
||||||
|
if (dOpts.getVendorId() == null) {
|
||||||
|
throw new IllegalArgumentException("Vendor ID cannot be null");
|
||||||
|
}
|
||||||
|
if (dOpts.getProductId() == null) {
|
||||||
|
throw new IllegalArgumentException("Product ID cannot be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
List<HidDeviceInfo> devList = PureJavaHidApi.enumerateDevices();
|
||||||
|
for(HidDeviceInfo device : devList) {
|
||||||
|
if (device.getVendorId() == dOpts.getVendorId().shortValue() && device.getProductId() == dOpts.getProductId().shortValue()
|
||||||
|
&& (dOpts.getUsagePage() == null || dOpts.getUsagePage().shortValue() == device.getUsagePage())
|
||||||
|
&& (dOpts.getSerial() == null || dOpts.getSerial().equals(device.getSerialNumberString()))) {
|
||||||
|
return device;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
297
old code/tray/src/qz/communication/SerialIO.java
Executable file
297
old code/tray/src/qz/communication/SerialIO.java
Executable file
@@ -0,0 +1,297 @@
|
|||||||
|
package qz.communication;
|
||||||
|
|
||||||
|
import jssc.*;
|
||||||
|
import org.apache.commons.codec.binary.StringUtils;
|
||||||
|
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.common.ByteArrayBuilder;
|
||||||
|
import qz.utils.ByteUtilities;
|
||||||
|
import qz.utils.DeviceUtilities;
|
||||||
|
import qz.ws.SocketConnection;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Tres
|
||||||
|
*/
|
||||||
|
public class SerialIO implements DeviceListener {
|
||||||
|
|
||||||
|
private static final Logger log = LogManager.getLogger(SerialIO.class);
|
||||||
|
|
||||||
|
// Timeout to wait before giving up on reading the specified amount of bytes
|
||||||
|
private static final int TIMEOUT = 1200;
|
||||||
|
|
||||||
|
private String portName;
|
||||||
|
private SerialPort port;
|
||||||
|
private SerialOptions serialOpts;
|
||||||
|
|
||||||
|
private ByteArrayBuilder data = new ByteArrayBuilder();
|
||||||
|
|
||||||
|
private SocketConnection websocket;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controller for serial communications
|
||||||
|
*
|
||||||
|
* @param portName Port name to open, such as "COM1" or "/dev/tty0/"
|
||||||
|
*/
|
||||||
|
public SerialIO(String portName, SocketConnection websocket) {
|
||||||
|
this.portName = portName;
|
||||||
|
this.websocket = websocket;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open the specified port name.
|
||||||
|
*
|
||||||
|
* @param opts Parsed serial options
|
||||||
|
* @return Boolean indicating success.
|
||||||
|
* @throws SerialPortException If the port fails to open.
|
||||||
|
*/
|
||||||
|
public boolean open(SerialOptions opts) throws SerialPortException {
|
||||||
|
if (isOpen()) {
|
||||||
|
log.warn("Serial port [{}] is already open", portName);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
port = new SerialPort(portName);
|
||||||
|
port.openPort();
|
||||||
|
|
||||||
|
serialOpts = new SerialOptions();
|
||||||
|
setOptions(opts);
|
||||||
|
|
||||||
|
return port.isOpened();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void applyPortListener(SerialPortEventListener listener) throws SerialPortException {
|
||||||
|
port.addEventListener(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Boolean indicating if port is currently open.
|
||||||
|
*/
|
||||||
|
public boolean isOpen() {
|
||||||
|
return port != null && port.isOpened();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String processSerialEvent(SerialPortEvent event) {
|
||||||
|
SerialOptions.ResponseFormat format = serialOpts.getResponseFormat();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Receive data
|
||||||
|
if (event.isRXCHAR()) {
|
||||||
|
data.append(port.readBytes(event.getEventValue(), TIMEOUT));
|
||||||
|
|
||||||
|
String response = null;
|
||||||
|
if (format.isBoundNewline()) {
|
||||||
|
//process as line delimited
|
||||||
|
|
||||||
|
// check for CR AND NL
|
||||||
|
Integer endIdx = ByteUtilities.firstMatchingIndex(data.getByteArray(), new byte[] {'\r', '\n'});
|
||||||
|
int delimSize = 2;
|
||||||
|
|
||||||
|
// check for CR OR NL
|
||||||
|
if(endIdx == null) {
|
||||||
|
endIdx = min(
|
||||||
|
ByteUtilities.firstMatchingIndex(data.getByteArray(), new byte[] {'\r'}),
|
||||||
|
ByteUtilities.firstMatchingIndex(data.getByteArray(), new byte[] {'\n'}));
|
||||||
|
delimSize = 1;
|
||||||
|
}
|
||||||
|
if (endIdx != null) {
|
||||||
|
log.trace("Reading newline-delimited response");
|
||||||
|
byte[] output = new byte[endIdx];
|
||||||
|
System.arraycopy(data.getByteArray(), 0, output, 0, endIdx);
|
||||||
|
String buffer = new String(output, format.getEncoding());
|
||||||
|
|
||||||
|
if (!buffer.isEmpty()) {
|
||||||
|
//send non-empty string
|
||||||
|
response = buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
data.clearRange(0, endIdx + delimSize);
|
||||||
|
}
|
||||||
|
} else if (format.getBoundStart() != null && format.getBoundStart().length > 0) {
|
||||||
|
//process as formatted response
|
||||||
|
Integer startIdx = ByteUtilities.firstMatchingIndex(data.getByteArray(), format.getBoundStart());
|
||||||
|
|
||||||
|
if (startIdx != null) {
|
||||||
|
int startOffset = startIdx + format.getBoundStart().length;
|
||||||
|
|
||||||
|
int copyLength = 0;
|
||||||
|
int endIdx = 0;
|
||||||
|
|
||||||
|
if (format.getBoundEnd() != null && format.getBoundEnd().length > 0) {
|
||||||
|
//process as bounded response
|
||||||
|
Integer boundEnd = ByteUtilities.firstMatchingIndex(data.getByteArray(), format.getBoundEnd(), startIdx);
|
||||||
|
|
||||||
|
if (boundEnd != null) {
|
||||||
|
log.trace("Reading bounded response");
|
||||||
|
|
||||||
|
copyLength = boundEnd - startOffset;
|
||||||
|
endIdx = boundEnd + 1;
|
||||||
|
if (format.isIncludeStart()) {
|
||||||
|
//also include the ending bytes
|
||||||
|
copyLength += format.getBoundEnd().length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (format.getFixedWidth() > 0) {
|
||||||
|
//process as fixed length prefixed response
|
||||||
|
log.trace("Reading fixed length prefixed response");
|
||||||
|
|
||||||
|
copyLength = format.getFixedWidth();
|
||||||
|
endIdx = startOffset + format.getFixedWidth();
|
||||||
|
} else if (format.getLength() != null) {
|
||||||
|
//process as dynamic formatted response
|
||||||
|
SerialOptions.ByteParam lengthParam = format.getLength();
|
||||||
|
|
||||||
|
if (data.getLength() > startOffset + lengthParam.getIndex() + lengthParam.getLength()) { //ensure there's length bytes to read
|
||||||
|
log.trace("Reading dynamic formatted response");
|
||||||
|
|
||||||
|
int expectedLength = ByteUtilities.parseBytes(data.getByteArray(), startOffset + lengthParam.getIndex(), lengthParam.getLength(), lengthParam.getEndian());
|
||||||
|
log.trace("Found length byte, expecting {} bytes", expectedLength);
|
||||||
|
|
||||||
|
startOffset += lengthParam.getIndex() + lengthParam.getLength(); // don't include the length byte(s) in the response
|
||||||
|
copyLength = expectedLength;
|
||||||
|
endIdx = startOffset + copyLength;
|
||||||
|
|
||||||
|
if (format.getCrc() != null) {
|
||||||
|
SerialOptions.ByteParam crcParam = format.getCrc();
|
||||||
|
|
||||||
|
log.trace("Expecting {} crc bytes", crcParam.getLength());
|
||||||
|
int expand = crcParam.getIndex() + crcParam.getLength();
|
||||||
|
|
||||||
|
//include crc in copy
|
||||||
|
copyLength += expand;
|
||||||
|
endIdx += expand;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
//process as header formatted raw response - high risk of lost data, likely unintended settings
|
||||||
|
log.warn("Reading header formatted raw response, are you missing an rx option?");
|
||||||
|
|
||||||
|
copyLength = data.getLength() - startOffset;
|
||||||
|
endIdx = data.getLength();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (copyLength > 0 && data.getLength() >= endIdx) {
|
||||||
|
log.debug("Response format readable, starting copy");
|
||||||
|
|
||||||
|
if (format.isIncludeStart()) {
|
||||||
|
//increase length to account for header bytes and bump offset back to include in copy
|
||||||
|
copyLength += (startOffset - startIdx);
|
||||||
|
startOffset = startIdx;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] responseData = new byte[copyLength];
|
||||||
|
System.arraycopy(data.getByteArray(), startOffset, responseData, 0, copyLength);
|
||||||
|
|
||||||
|
response = new String(responseData, format.getEncoding());
|
||||||
|
data.clearRange(startIdx, endIdx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (format.getFixedWidth() > 0) {
|
||||||
|
if (data.getLength() >= format.getFixedWidth()) {
|
||||||
|
//process as fixed width response
|
||||||
|
log.trace("Reading fixed length response");
|
||||||
|
|
||||||
|
byte[] output = new byte[format.getFixedWidth()];
|
||||||
|
System.arraycopy(data.getByteArray(), 0, output, 0, format.getFixedWidth());
|
||||||
|
|
||||||
|
response = StringUtils.newStringUtf8(output);
|
||||||
|
data.clearRange(0, format.getFixedWidth());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
//no processing, return raw
|
||||||
|
log.trace("Reading raw response");
|
||||||
|
|
||||||
|
response = new String(data.getByteArray(), format.getEncoding());
|
||||||
|
data.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(SerialPortException e) {
|
||||||
|
log.error("Exception occurred while reading data from port.", e);
|
||||||
|
}
|
||||||
|
catch(SerialPortTimeoutException e) {
|
||||||
|
log.error("Timeout occurred waiting for port to respond.", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets and caches the properties as to not set them every data call
|
||||||
|
*
|
||||||
|
* @throws SerialPortException If the properties fail to set
|
||||||
|
*/
|
||||||
|
private void setOptions(SerialOptions opts) throws SerialPortException {
|
||||||
|
if (opts == null) { return; }
|
||||||
|
|
||||||
|
SerialOptions.PortSettings ps = opts.getPortSettings();
|
||||||
|
if (ps != null && !ps.equals(serialOpts.getPortSettings())) {
|
||||||
|
log.debug("Applying new port settings");
|
||||||
|
port.setParams(ps.getBaudRate(), ps.getDataBits(), ps.getStopBits(), ps.getParity());
|
||||||
|
port.setFlowControlMode(ps.getFlowControl());
|
||||||
|
serialOpts.setPortSettings(ps);
|
||||||
|
}
|
||||||
|
|
||||||
|
SerialOptions.ResponseFormat rf = opts.getResponseFormat();
|
||||||
|
if (rf != null) {
|
||||||
|
log.debug("Applying new response formatting");
|
||||||
|
serialOpts.setResponseFormat(rf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies the port parameters and writes the buffered data to the serial port.
|
||||||
|
*/
|
||||||
|
public void sendData(JSONObject params, SerialOptions opts) throws JSONException, IOException, SerialPortException {
|
||||||
|
if (opts != null) {
|
||||||
|
setOptions(opts);
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug("Sending data over [{}]", portName);
|
||||||
|
port.writeBytes(DeviceUtilities.getDataBytes(params, serialOpts.getPortSettings().getEncoding()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes the serial port, if open.
|
||||||
|
*
|
||||||
|
* @throws SerialPortException If the port fails to close.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
// Remove orphaned reference
|
||||||
|
websocket.removeSerialPort(portName);
|
||||||
|
|
||||||
|
if (!isOpen()) {
|
||||||
|
log.warn("Serial port [{}] is not open.", portName);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
boolean closed = port.closePort();
|
||||||
|
if (closed) {
|
||||||
|
log.info("Serial port [{}] closed successfully.", portName);
|
||||||
|
} else {
|
||||||
|
// Handle ambiguity in JSSCs API
|
||||||
|
throw new SerialPortException(portName, "closePort", "Port not closed");
|
||||||
|
}
|
||||||
|
} catch(SerialPortException e) {
|
||||||
|
log.warn("Serial port [{}] was not closed properly.", portName);
|
||||||
|
}
|
||||||
|
|
||||||
|
port = null;
|
||||||
|
portName = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Integer min(Integer a, Integer b) {
|
||||||
|
if (a == null) { return b; }
|
||||||
|
if (b == null) { return a; }
|
||||||
|
return Math.min(a, b);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
350
old code/tray/src/qz/communication/SerialOptions.java
Executable file
350
old code/tray/src/qz/communication/SerialOptions.java
Executable file
@@ -0,0 +1,350 @@
|
|||||||
|
package qz.communication;
|
||||||
|
|
||||||
|
import jssc.SerialPort;
|
||||||
|
import org.apache.commons.lang3.ArrayUtils;
|
||||||
|
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.utils.ByteUtilities;
|
||||||
|
import qz.utils.DeviceUtilities;
|
||||||
|
import qz.utils.LoggerUtilities;
|
||||||
|
import qz.utils.SerialUtilities;
|
||||||
|
|
||||||
|
import java.nio.charset.Charset;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
public class SerialOptions {
|
||||||
|
|
||||||
|
private static final Logger log = LogManager.getLogger(SerialOptions.class);
|
||||||
|
|
||||||
|
private static final String DEFAULT_BEGIN = "0x0002";
|
||||||
|
private static final String DEFAULT_END = "0x000D";
|
||||||
|
|
||||||
|
private PortSettings portSettings = null;
|
||||||
|
private ResponseFormat responseFormat = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an empty/default options object
|
||||||
|
*/
|
||||||
|
public SerialOptions() {
|
||||||
|
portSettings = new PortSettings();
|
||||||
|
responseFormat = new ResponseFormat();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the provided JSON object into relevant SerialPort constants
|
||||||
|
*/
|
||||||
|
public SerialOptions(JSONObject serialOpts, boolean isOpening) {
|
||||||
|
if (serialOpts == null) { return; }
|
||||||
|
|
||||||
|
//only apply port settings if opening or explicitly set in a send data call
|
||||||
|
if (isOpening || serialOpts.has("baudRate") || serialOpts.has("dataBits") || serialOpts.has("stopBits") || serialOpts.has("parity") || serialOpts.has("flowControl")) {
|
||||||
|
portSettings = new PortSettings();
|
||||||
|
|
||||||
|
if (!serialOpts.isNull("baudRate")) {
|
||||||
|
try { portSettings.baudRate = SerialUtilities.parseBaudRate(serialOpts.getString("baudRate")); }
|
||||||
|
catch(JSONException e) { LoggerUtilities.optionWarn(log, "string", "baudRate", serialOpts.opt("baudRate")); }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!serialOpts.isNull("dataBits")) {
|
||||||
|
try { portSettings.dataBits = SerialUtilities.parseDataBits(serialOpts.getString("dataBits")); }
|
||||||
|
catch(JSONException e) { LoggerUtilities.optionWarn(log, "string", "dataBits", serialOpts.opt("dataBits")); }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!serialOpts.isNull("stopBits")) {
|
||||||
|
try { portSettings.stopBits = SerialUtilities.parseStopBits(serialOpts.getString("stopBits")); }
|
||||||
|
catch(JSONException e) { LoggerUtilities.optionWarn(log, "string", "stopBits", serialOpts.opt("stopBits")); }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!serialOpts.isNull("parity")) {
|
||||||
|
try { portSettings.parity = SerialUtilities.parseParity(serialOpts.getString("parity")); }
|
||||||
|
catch(JSONException e) { LoggerUtilities.optionWarn(log, "string", "parity", serialOpts.opt("parity")); }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!serialOpts.isNull("flowControl")) {
|
||||||
|
try { portSettings.flowControl = SerialUtilities.parseFlowControl(serialOpts.getString("flowControl")); }
|
||||||
|
catch(JSONException e) { LoggerUtilities.optionWarn(log, "string", "flowControl", serialOpts.opt("flowControl")); }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!serialOpts.isNull("encoding") && !serialOpts.optString("encoding").isEmpty()) {
|
||||||
|
try { portSettings.encoding = Charset.forName(serialOpts.getString("encoding")); }
|
||||||
|
catch(JSONException e) { LoggerUtilities.optionWarn(log, "string", "encoding", serialOpts.opt("encoding")); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!serialOpts.isNull("rx")) {
|
||||||
|
responseFormat = new ResponseFormat();
|
||||||
|
//Make the response encoding default to the port encoding. If this is removed it will default to UTF-8
|
||||||
|
responseFormat.encoding = portSettings.encoding;
|
||||||
|
|
||||||
|
JSONObject respOpts = serialOpts.optJSONObject("rx");
|
||||||
|
if (respOpts != null) {
|
||||||
|
if (!respOpts.isNull("start")) {
|
||||||
|
try {
|
||||||
|
JSONArray startBits = respOpts.getJSONArray("start");
|
||||||
|
ArrayList<Byte> bytes = new ArrayList<>();
|
||||||
|
for(int i = 0; i < startBits.length(); i++) {
|
||||||
|
byte[] charByte = DeviceUtilities.characterBytes(startBits.getString(i), responseFormat.encoding);
|
||||||
|
for(byte b : charByte) { bytes.add(b); }
|
||||||
|
}
|
||||||
|
responseFormat.boundStart = ArrayUtils.toPrimitive(bytes.toArray(new Byte[0]));
|
||||||
|
}
|
||||||
|
catch(JSONException e) {
|
||||||
|
try { responseFormat.boundStart = DeviceUtilities.characterBytes(respOpts.getString("start"), responseFormat.encoding); }
|
||||||
|
catch(JSONException e2) { LoggerUtilities.optionWarn(log, "string", "start", respOpts.opt("start")); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!respOpts.isNull("includeHeader")) {
|
||||||
|
try { responseFormat.includeStart = respOpts.getBoolean("includeHeader"); }
|
||||||
|
catch(JSONException e) { LoggerUtilities.optionWarn(log, "boolean", "includeHeader", respOpts.opt("includeHeader")); }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!respOpts.isNull("end")) {
|
||||||
|
try { responseFormat.boundEnd = DeviceUtilities.characterBytes(respOpts.getString("end"), responseFormat.encoding); }
|
||||||
|
catch(JSONException e) { LoggerUtilities.optionWarn(log, "string", "end", respOpts.opt("end")); }
|
||||||
|
|
||||||
|
if (responseFormat.boundStart == null || responseFormat.boundStart.length == 0) {
|
||||||
|
log.warn("End bound set without start bound defined");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!respOpts.isNull("untilNewline")) {
|
||||||
|
try { responseFormat.boundNewline = respOpts.getBoolean("untilNewline"); }
|
||||||
|
catch(JSONException e) { LoggerUtilities.optionWarn(log, "boolean", "untilNewline", respOpts.opt("untilNewline")); }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!respOpts.isNull("width")) {
|
||||||
|
try { responseFormat.fixedWidth = respOpts.getInt("width"); }
|
||||||
|
catch(JSONException e) { LoggerUtilities.optionWarn(log, "integer", "width", respOpts.opt("width")); }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!respOpts.isNull("lengthBytes")) {
|
||||||
|
try {
|
||||||
|
JSONObject lengthOpts = respOpts.optJSONObject("lengthBytes");
|
||||||
|
responseFormat.length = new ByteParam();
|
||||||
|
|
||||||
|
if (lengthOpts != null) {
|
||||||
|
if (!lengthOpts.isNull("index")) {
|
||||||
|
try { responseFormat.length.index = lengthOpts.getInt("index"); }
|
||||||
|
catch(JSONException se) { LoggerUtilities.optionWarn(log, "integer", "lengthBytes.index", lengthOpts.opt("index")); }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!lengthOpts.isNull("length")) {
|
||||||
|
try { responseFormat.length.length = lengthOpts.getInt("length"); }
|
||||||
|
catch(JSONException se) { LoggerUtilities.optionWarn(log, "integer", "lengthBytes.length", lengthOpts.opt("length")); }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!lengthOpts.isNull("endian")) {
|
||||||
|
try { responseFormat.length.endian = ByteUtilities.Endian.valueOf(lengthOpts.getString("endian").toUpperCase(Locale.ENGLISH)); }
|
||||||
|
catch(JSONException se) { LoggerUtilities.optionWarn(log, "string", "lengthBytes.endian", lengthOpts.opt("endian")); }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
responseFormat.length.index = respOpts.getInt("lengthBytes");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(JSONException e) { LoggerUtilities.optionWarn(log, "integer", "lengthBytes", respOpts.opt("lengthBytes")); }
|
||||||
|
|
||||||
|
if (responseFormat.boundStart == null || responseFormat.boundStart.length == 0) {
|
||||||
|
log.warn("Length byte(s) defined without start bound defined");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!respOpts.isNull("crcBytes")) {
|
||||||
|
try {
|
||||||
|
JSONObject crcOpts = respOpts.optJSONObject("crcBytes");
|
||||||
|
responseFormat.crc = new ByteParam();
|
||||||
|
|
||||||
|
if (crcOpts != null) {
|
||||||
|
if (!crcOpts.isNull("index")) {
|
||||||
|
try { responseFormat.crc.index = crcOpts.getInt("index"); }
|
||||||
|
catch(JSONException se) { LoggerUtilities.optionWarn(log, "integer", "crcBytes.index", crcOpts.opt("index")); }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!crcOpts.isNull("length")) {
|
||||||
|
try { responseFormat.crc.length = crcOpts.getInt("length"); }
|
||||||
|
catch(JSONException se) { LoggerUtilities.optionWarn(log, "integer", "crcBytes.length", crcOpts.opt("length")); }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
responseFormat.crc.length = respOpts.getInt("crcBytes");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(JSONException e) { LoggerUtilities.optionWarn(log, "integer", "crcBytes", respOpts.opt("crcBytes")); }
|
||||||
|
|
||||||
|
if (responseFormat.boundStart == null || responseFormat.boundStart.length == 0) {
|
||||||
|
log.warn("CRC byte(s) defined without start bound defined");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!respOpts.isNull("encoding") && !respOpts.optString("encoding").isEmpty()) {
|
||||||
|
try { responseFormat.encoding = Charset.forName(respOpts.getString("encoding")); }
|
||||||
|
catch(JSONException | IllegalArgumentException e) { LoggerUtilities.optionWarn(log, "charset", "encoding", respOpts.opt("encoding")); }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LoggerUtilities.optionWarn(log, "JSONObject", "rx", serialOpts.opt("rx"));
|
||||||
|
}
|
||||||
|
} else if (isOpening) {
|
||||||
|
// legacy support - only applies on port open
|
||||||
|
responseFormat = new ResponseFormat();
|
||||||
|
|
||||||
|
// legacy start only supports string, not an array
|
||||||
|
if (!serialOpts.isNull("start")) {
|
||||||
|
responseFormat.boundStart = DeviceUtilities.characterBytes(serialOpts.optString("start", DEFAULT_BEGIN), responseFormat.encoding);
|
||||||
|
} else {
|
||||||
|
responseFormat.boundStart = DeviceUtilities.characterBytes(DEFAULT_BEGIN, responseFormat.encoding);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!serialOpts.isNull("end")) {
|
||||||
|
responseFormat.boundEnd = DeviceUtilities.characterBytes(serialOpts.optString("end", DEFAULT_END), responseFormat.encoding);
|
||||||
|
} else {
|
||||||
|
responseFormat.boundEnd = DeviceUtilities.characterBytes(DEFAULT_END, responseFormat.encoding);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!serialOpts.isNull("width")) {
|
||||||
|
try {
|
||||||
|
responseFormat.fixedWidth = serialOpts.getInt("width");
|
||||||
|
if (responseFormat.boundEnd.length > 0) {
|
||||||
|
log.warn("Combining 'width' property with 'end' property has undefined behavior and should not be used");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(JSONException e) { LoggerUtilities.optionWarn(log, "integer", "width", serialOpts.opt("width")); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public PortSettings getPortSettings() {
|
||||||
|
return portSettings;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ResponseFormat getResponseFormat() {
|
||||||
|
return responseFormat;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPortSettings(PortSettings portSettings) {
|
||||||
|
this.portSettings = portSettings;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setResponseFormat(ResponseFormat responseFormat) {
|
||||||
|
this.responseFormat = responseFormat;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PortSettings {
|
||||||
|
|
||||||
|
private Charset encoding = Charset.forName("UTF-8");
|
||||||
|
private int baudRate = SerialPort.BAUDRATE_9600;
|
||||||
|
private int dataBits = SerialPort.DATABITS_8;
|
||||||
|
private int stopBits = SerialPort.STOPBITS_1;
|
||||||
|
private int parity = SerialPort.PARITY_NONE;
|
||||||
|
private int flowControl = SerialPort.FLOWCONTROL_NONE;
|
||||||
|
|
||||||
|
|
||||||
|
public Charset getEncoding() {
|
||||||
|
return encoding;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getBaudRate() {
|
||||||
|
return baudRate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getDataBits() {
|
||||||
|
return dataBits;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getStopBits() {
|
||||||
|
return stopBits;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getParity() {
|
||||||
|
return parity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getFlowControl() {
|
||||||
|
return flowControl;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (o instanceof PortSettings) {
|
||||||
|
PortSettings that = (PortSettings)o;
|
||||||
|
|
||||||
|
return getEncoding().equals(that.getEncoding()) &&
|
||||||
|
getBaudRate() == that.getBaudRate() &&
|
||||||
|
getDataBits() == that.getDataBits() &&
|
||||||
|
getStopBits() == that.getStopBits() &&
|
||||||
|
getParity() == that.getParity() &&
|
||||||
|
getFlowControl() == that.getFlowControl();
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ResponseFormat {
|
||||||
|
|
||||||
|
private Charset encoding = Charset.forName("UTF-8"); //Response charset
|
||||||
|
private byte[] boundStart; //Character(s) denoting start of new response
|
||||||
|
private byte[] boundEnd; //Character denoting end of a response
|
||||||
|
private boolean boundNewline; //If the response should be split on \r?\n
|
||||||
|
private int fixedWidth; //Fixed length response bounds
|
||||||
|
private ByteParam length; //Info about the data length byte(s)
|
||||||
|
private ByteParam crc; //Info about the data crc byte(s)
|
||||||
|
private boolean includeStart; //If the response headers should be sent as well
|
||||||
|
|
||||||
|
|
||||||
|
public Charset getEncoding() {
|
||||||
|
return encoding;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getBoundStart() {
|
||||||
|
return boundStart;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getBoundEnd() {
|
||||||
|
return boundEnd;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getFixedWidth() {
|
||||||
|
return fixedWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ByteParam getLength() {
|
||||||
|
return length;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ByteParam getCrc() {
|
||||||
|
return crc;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isIncludeStart() {
|
||||||
|
return includeStart;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isBoundNewline() {
|
||||||
|
return boundNewline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ByteParam {
|
||||||
|
|
||||||
|
private int index = 0;
|
||||||
|
private int length = 1;
|
||||||
|
private ByteUtilities.Endian endian = ByteUtilities.Endian.BIG;
|
||||||
|
|
||||||
|
|
||||||
|
public int getIndex() {
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getLength() {
|
||||||
|
return length;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ByteUtilities.Endian getEndian() {
|
||||||
|
return endian;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
100
old code/tray/src/qz/communication/SocketIO.java
Executable file
100
old code/tray/src/qz/communication/SocketIO.java
Executable file
@@ -0,0 +1,100 @@
|
|||||||
|
package qz.communication;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.ArrayUtils;
|
||||||
|
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.utils.DeviceUtilities;
|
||||||
|
import qz.utils.NetworkUtilities;
|
||||||
|
import qz.ws.SocketConnection;
|
||||||
|
|
||||||
|
import java.io.DataInputStream;
|
||||||
|
import java.io.DataOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.Socket;
|
||||||
|
import java.nio.charset.Charset;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
|
||||||
|
public class SocketIO implements DeviceListener {
|
||||||
|
|
||||||
|
private static final Logger log = LogManager.getLogger(SocketIO.class);
|
||||||
|
|
||||||
|
private String host;
|
||||||
|
private int port;
|
||||||
|
private Charset encoding;
|
||||||
|
|
||||||
|
private Socket socket;
|
||||||
|
private DataOutputStream dataOut;
|
||||||
|
private DataInputStream dataIn;
|
||||||
|
|
||||||
|
private SocketConnection websocket;
|
||||||
|
|
||||||
|
public SocketIO(String host, int port, Charset encoding, SocketConnection websocket) {
|
||||||
|
this.host = host;
|
||||||
|
this.port = port;
|
||||||
|
this.encoding = encoding;
|
||||||
|
this.websocket = websocket;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean open() throws IOException {
|
||||||
|
socket = new Socket(host, port);
|
||||||
|
socket.setSoTimeout(NetworkUtilities.SOCKET_TIMEOUT);
|
||||||
|
dataOut = new DataOutputStream(socket.getOutputStream());
|
||||||
|
dataIn = new DataInputStream(socket.getInputStream());
|
||||||
|
|
||||||
|
return socket.isConnected();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isOpen() {
|
||||||
|
return socket.isConnected();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendData(JSONObject params) throws JSONException, IOException {
|
||||||
|
log.debug("Sending data over [{}:{}]", host, port);
|
||||||
|
dataOut.write(DeviceUtilities.getDataBytes(params, encoding));
|
||||||
|
dataOut.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String processSocketResponse() throws IOException {
|
||||||
|
byte[] response = new byte[1024];
|
||||||
|
ArrayList<Byte> fullResponse = new ArrayList<>();
|
||||||
|
do {
|
||||||
|
int size = dataIn.read(response);
|
||||||
|
for(int i = 0; i < size; i++) {
|
||||||
|
fullResponse.add(response[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
while(dataIn.available() > 0);
|
||||||
|
if(fullResponse.size() > 0) {
|
||||||
|
return new String(ArrayUtils.toPrimitive(fullResponse.toArray(new Byte[0])), encoding);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
// Remove orphaned reference
|
||||||
|
websocket.removeNetworkSocket(String.format("%s:%s", host, port));
|
||||||
|
|
||||||
|
try {
|
||||||
|
dataOut.close();
|
||||||
|
} catch(IOException e) {
|
||||||
|
log.warn("Could not close socket output stream", e);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
socket.close();
|
||||||
|
} catch(IOException e) {
|
||||||
|
log.warn("Could not close socket", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getHost() {
|
||||||
|
return host;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getPort() {
|
||||||
|
return port;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
153
old code/tray/src/qz/communication/UsbIO.java
Executable file
153
old code/tray/src/qz/communication/UsbIO.java
Executable file
@@ -0,0 +1,153 @@
|
|||||||
|
package qz.communication;
|
||||||
|
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
import qz.utils.UsbUtilities;
|
||||||
|
import qz.ws.SocketConnection;
|
||||||
|
|
||||||
|
import javax.usb.*;
|
||||||
|
import javax.usb.util.UsbUtil;
|
||||||
|
|
||||||
|
public class UsbIO implements DeviceIO {
|
||||||
|
private static final Logger log = LogManager.getLogger(UsbIO.class);
|
||||||
|
private UsbDevice device;
|
||||||
|
private UsbInterface iface;
|
||||||
|
|
||||||
|
private boolean streaming;
|
||||||
|
|
||||||
|
private DeviceOptions dOpts;
|
||||||
|
private SocketConnection websocket;
|
||||||
|
|
||||||
|
public UsbIO(DeviceOptions dOpts, SocketConnection websocket) throws DeviceException {
|
||||||
|
this.dOpts = dOpts;
|
||||||
|
this.websocket = websocket;
|
||||||
|
UsbDevice device = UsbUtilities.findDevice(dOpts.getVendorId().shortValue(), dOpts.getProductId().shortValue());
|
||||||
|
if (device == null) {
|
||||||
|
throw new DeviceException("USB device could not be found");
|
||||||
|
}
|
||||||
|
if (dOpts.getInterfaceId() == null) {
|
||||||
|
throw new IllegalArgumentException("Device interface cannot be null");
|
||||||
|
}
|
||||||
|
this.iface = device.getActiveUsbConfiguration().getUsbInterface(dOpts.getInterfaceId());
|
||||||
|
if (iface == null) {
|
||||||
|
throw new DeviceException(String.format("Could not find USB interface matching [ vendorId: '%s', productId: '%s', interface: '%s' ]",
|
||||||
|
"0x" + UsbUtil.toHexString(dOpts.getVendorId()),
|
||||||
|
"0x" + UsbUtil.toHexString(dOpts.getProductId()),
|
||||||
|
"0x" + UsbUtil.toHexString(dOpts.getInterfaceId())));
|
||||||
|
}
|
||||||
|
this.device = device;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public void open() throws DeviceException {
|
||||||
|
try {
|
||||||
|
iface.claim(new UsbInterfacePolicy() {
|
||||||
|
@Override
|
||||||
|
public boolean forceClaim(UsbInterface usbInterface) {
|
||||||
|
// Releases kernel driver for systems that auto-claim usb devices
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch(UsbException e) {
|
||||||
|
throw new DeviceException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isOpen() {
|
||||||
|
return iface.isClaimed();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStreaming(boolean active) {
|
||||||
|
streaming = active;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isStreaming() {
|
||||||
|
return streaming;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getVendorId() {
|
||||||
|
return UsbUtil.toHexString(device.getUsbDeviceDescriptor().idVendor());
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getProductId() {
|
||||||
|
return UsbUtil.toHexString(device.getUsbDeviceDescriptor().idProduct());
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getInterface() {
|
||||||
|
return UsbUtil.toHexString(iface.getUsbInterfaceDescriptor().iInterface());
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] readData(int responseSize, Byte endpoint) throws DeviceException {
|
||||||
|
try {
|
||||||
|
byte[] response = new byte[responseSize];
|
||||||
|
exchangeData(endpoint, response);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
catch(UsbException e) {
|
||||||
|
throw new DeviceException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendData(byte[] data, Byte endpoint) throws DeviceException {
|
||||||
|
try {
|
||||||
|
exchangeData(endpoint, data);
|
||||||
|
}
|
||||||
|
catch(UsbException e) {
|
||||||
|
throw new DeviceException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getFeatureReport(int responseSize, Byte reportId) throws DeviceException {
|
||||||
|
throw new DeviceException("USB feature reports are not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendFeatureReport(byte[] data, Byte reportId) throws DeviceException {
|
||||||
|
throw new DeviceException("USB feature reports are not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data will be sent to or received from the open usb device, depending on the {@code endpoint} used.
|
||||||
|
*
|
||||||
|
* @param endpoint Endpoint on the usb device interface to pass data across
|
||||||
|
* @param data Byte array of data to send, or to be written from a receive
|
||||||
|
*/
|
||||||
|
private synchronized void exchangeData(Byte endpoint, byte[] data) throws UsbException, DeviceException {
|
||||||
|
if (endpoint == null) {
|
||||||
|
throw new IllegalArgumentException("Interface endpoint cannot be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
UsbEndpoint usbEndpoint = iface.getUsbEndpoint(endpoint);
|
||||||
|
if(usbEndpoint == null) {
|
||||||
|
throw new DeviceException(String.format("Could not find USB endpoint matching [ endpoint: '%s' ]",
|
||||||
|
"0x" + UsbUtil.toHexString(endpoint)));
|
||||||
|
}
|
||||||
|
UsbPipe pipe = usbEndpoint.getUsbPipe();
|
||||||
|
if (!pipe.isOpen()) { pipe.open(); }
|
||||||
|
|
||||||
|
try {
|
||||||
|
pipe.syncSubmit(data);
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
if(pipe != null) {
|
||||||
|
pipe.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
setStreaming(false);
|
||||||
|
// Remove orphaned reference
|
||||||
|
websocket.removeDevice(dOpts);
|
||||||
|
if (iface.isClaimed()) {
|
||||||
|
try {
|
||||||
|
iface.release();
|
||||||
|
}
|
||||||
|
catch(UsbException e) {
|
||||||
|
log.error("Unable to close USB device", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
28
old code/tray/src/qz/communication/WinspoolEx.java
Executable file
28
old code/tray/src/qz/communication/WinspoolEx.java
Executable file
@@ -0,0 +1,28 @@
|
|||||||
|
package qz.communication;
|
||||||
|
|
||||||
|
import com.sun.jna.Native;
|
||||||
|
import com.sun.jna.Pointer;
|
||||||
|
import com.sun.jna.platform.win32.WinNT;
|
||||||
|
import com.sun.jna.platform.win32.Winspool;
|
||||||
|
import com.sun.jna.win32.W32APIOptions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO: Remove when JNA 5.14.0+ is bundled
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
public interface WinspoolEx extends Winspool {
|
||||||
|
WinspoolEx INSTANCE = Native.load("Winspool.drv", WinspoolEx.class, W32APIOptions.DEFAULT_OPTIONS);
|
||||||
|
|
||||||
|
int JOB_CONTROL_NONE = 0x00000000; // Perform no additional action.
|
||||||
|
int JOB_CONTROL_PAUSE = 0x00000001; // Pause the print job.
|
||||||
|
int JOB_CONTROL_RESUME = 0x00000002; // Resume a paused print job.
|
||||||
|
int JOB_CONTROL_CANCEL = 0x00000003; // Delete a print job.
|
||||||
|
int JOB_CONTROL_RESTART = 0x00000004; // Restart a print job.
|
||||||
|
int JOB_CONTROL_DELETE = 0x00000005; // Delete a print job.
|
||||||
|
int JOB_CONTROL_SENT_TO_PRINTER = 0x00000006; // Used by port monitors to signal that a print job has been sent to the printer. This value SHOULD NOT be used remotely.
|
||||||
|
int JOB_CONTROL_LAST_PAGE_EJECTED = 0x00000007; // Used by language monitors to signal that the last page of a print job has been ejected from the printer. This value SHOULD NOT be used remotely.
|
||||||
|
int JOB_CONTROL_RETAIN = 0x00000008; // Keep the print job in the print queue after it prints.
|
||||||
|
int JOB_CONTROL_RELEASE = 0x00000009; // Release the print job, undoing the effect of a JOB_CONTROL_RETAIN action.
|
||||||
|
|
||||||
|
boolean SetJob(WinNT.HANDLE hPrinter, int JobId, int Level, Pointer pJob, int Command);
|
||||||
|
}
|
||||||
11
old code/tray/src/qz/exception/InvalidRawImageException.java
Executable file
11
old code/tray/src/qz/exception/InvalidRawImageException.java
Executable file
@@ -0,0 +1,11 @@
|
|||||||
|
package qz.exception;
|
||||||
|
|
||||||
|
public class InvalidRawImageException extends Exception {
|
||||||
|
public InvalidRawImageException(String msg) {
|
||||||
|
super(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
public InvalidRawImageException(String msg, Throwable cause) {
|
||||||
|
super(msg, cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
3
old code/tray/src/qz/exception/MissingArgException.java
Executable file
3
old code/tray/src/qz/exception/MissingArgException.java
Executable file
@@ -0,0 +1,3 @@
|
|||||||
|
package qz.exception;
|
||||||
|
|
||||||
|
public class MissingArgException extends Exception {}
|
||||||
10
old code/tray/src/qz/exception/NullCommandException.java
Executable file
10
old code/tray/src/qz/exception/NullCommandException.java
Executable file
@@ -0,0 +1,10 @@
|
|||||||
|
package qz.exception;
|
||||||
|
|
||||||
|
public class NullCommandException extends javax.print.PrintException {
|
||||||
|
public NullCommandException() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
public NullCommandException(String msg) {
|
||||||
|
super(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
7
old code/tray/src/qz/exception/NullPrintServiceException.java
Executable file
7
old code/tray/src/qz/exception/NullPrintServiceException.java
Executable file
@@ -0,0 +1,7 @@
|
|||||||
|
package qz.exception;
|
||||||
|
|
||||||
|
public class NullPrintServiceException extends javax.print.PrintException {
|
||||||
|
public NullPrintServiceException(String msg) {
|
||||||
|
super(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
413
old code/tray/src/qz/installer/Installer.java
Executable file
413
old code/tray/src/qz/installer/Installer.java
Executable file
@@ -0,0 +1,413 @@
|
|||||||
|
/**
|
||||||
|
* @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 qz.installer;
|
||||||
|
|
||||||
|
import org.apache.commons.io.FileUtils;
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
import qz.auth.Certificate;
|
||||||
|
import qz.build.provision.params.Phase;
|
||||||
|
import qz.installer.certificate.*;
|
||||||
|
import qz.installer.certificate.firefox.FirefoxCertificateInstaller;
|
||||||
|
import qz.installer.provision.ProvisionInstaller;
|
||||||
|
import qz.utils.FileUtilities;
|
||||||
|
import qz.utils.SystemUtilities;
|
||||||
|
import qz.ws.WebsocketPorts;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.nio.file.*;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
import static qz.common.Constants.*;
|
||||||
|
import static qz.installer.certificate.KeyPairWrapper.Type.CA;
|
||||||
|
import static qz.utils.FileUtilities.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cross-platform wrapper for install steps
|
||||||
|
* - Used by CommandParser via command line
|
||||||
|
* - Used by PrintSocketServer at startup to ensure SSL is functioning
|
||||||
|
*/
|
||||||
|
public abstract class Installer {
|
||||||
|
protected static final Logger log = LogManager.getLogger(Installer.class);
|
||||||
|
|
||||||
|
// Silence prompts within our control
|
||||||
|
public static boolean IS_SILENT = "1".equals(System.getenv(DATA_DIR + "_silent"));
|
||||||
|
public static String JRE_LOCATION = SystemUtilities.isMac() ? "Contents/PlugIns/Java.runtime/Contents/Home" : "runtime";
|
||||||
|
|
||||||
|
WebsocketPorts websocketPorts;
|
||||||
|
|
||||||
|
public enum PrivilegeLevel {
|
||||||
|
USER,
|
||||||
|
SYSTEM
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract Installer removeLegacyStartup();
|
||||||
|
public abstract Installer addAppLauncher();
|
||||||
|
public abstract Installer addStartupEntry();
|
||||||
|
public abstract Installer addSystemSettings();
|
||||||
|
public abstract Installer removeSystemSettings();
|
||||||
|
public abstract void spawn(List<String> args) throws Exception;
|
||||||
|
|
||||||
|
public abstract void setDestination(String destination);
|
||||||
|
public abstract String getDestination();
|
||||||
|
|
||||||
|
private static Installer instance;
|
||||||
|
|
||||||
|
public static Installer getInstance() {
|
||||||
|
if(instance == null) {
|
||||||
|
switch(SystemUtilities.getOs()) {
|
||||||
|
case WINDOWS:
|
||||||
|
instance = new WindowsInstaller();
|
||||||
|
break;
|
||||||
|
case MAC:
|
||||||
|
instance = new MacInstaller();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
instance = new LinuxInstaller();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void install(String destination, boolean silent) throws Exception {
|
||||||
|
IS_SILENT |= silent; // preserve environmental variable if possible
|
||||||
|
getInstance();
|
||||||
|
if (destination != null) {
|
||||||
|
instance.setDestination(destination);
|
||||||
|
}
|
||||||
|
install();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean preinstall() {
|
||||||
|
getInstance();
|
||||||
|
log.info("Fixing runtime permissions...");
|
||||||
|
instance.setJrePermissions(SystemUtilities.getAppPath().toString());
|
||||||
|
log.info("Stopping running instances...");
|
||||||
|
return TaskKiller.killAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void install() throws Exception {
|
||||||
|
getInstance();
|
||||||
|
log.info("Installing to {}", instance.getDestination());
|
||||||
|
instance.removeLibs()
|
||||||
|
.removeProvisioning()
|
||||||
|
.deployApp()
|
||||||
|
.removeLegacyStartup()
|
||||||
|
.removeLegacyFiles()
|
||||||
|
.addSharedDirectory()
|
||||||
|
.addAppLauncher()
|
||||||
|
.addStartupEntry()
|
||||||
|
.invokeProvisioning(Phase.INSTALL)
|
||||||
|
.addSystemSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void uninstall() {
|
||||||
|
log.info("Stopping running instances...");
|
||||||
|
TaskKiller.killAll();
|
||||||
|
getInstance();
|
||||||
|
log.info("Uninstalling from {}", instance.getDestination());
|
||||||
|
instance.removeSharedDirectory()
|
||||||
|
.removeSystemSettings()
|
||||||
|
.removeCerts()
|
||||||
|
.invokeProvisioning(Phase.UNINSTALL);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Installer deployApp() throws IOException {
|
||||||
|
Path src = SystemUtilities.getAppPath();
|
||||||
|
Path dest = Paths.get(getDestination());
|
||||||
|
|
||||||
|
if(!Files.exists(dest)) {
|
||||||
|
Files.createDirectories(dest);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the JDK blindly
|
||||||
|
FileUtils.deleteDirectory(dest.resolve(JRE_LOCATION).toFile());
|
||||||
|
// Note: preserveFileDate=false per https://github.com/qzind/tray/issues/1011
|
||||||
|
FileUtils.copyDirectory(src.toFile(), dest.toFile(), false);
|
||||||
|
FileUtilities.setPermissionsRecursively(dest, false);
|
||||||
|
// Fix permissions for provisioned files
|
||||||
|
FileUtilities.setExecutableRecursively(SystemUtilities.isMac() ?
|
||||||
|
dest.resolve("Contents/Resources").resolve(PROVISION_DIR) :
|
||||||
|
dest.resolve(PROVISION_DIR), false);
|
||||||
|
if(!SystemUtilities.isWindows()) {
|
||||||
|
setExecutable(SystemUtilities.isMac() ? "Contents/Resources/uninstall" : "uninstall");
|
||||||
|
setExecutable(SystemUtilities.isMac() ? "Contents/MacOS/" + ABOUT_TITLE : PROPS_FILE);
|
||||||
|
return setJrePermissions(getDestination());
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Installer setJrePermissions(String dest) {
|
||||||
|
File jreLocation = new File(dest, JRE_LOCATION);
|
||||||
|
File jreBin = new File(jreLocation, "bin");
|
||||||
|
File jreLib = new File(jreLocation, "lib");
|
||||||
|
|
||||||
|
// Set jre/bin/java and friends executable
|
||||||
|
File[] files = jreBin.listFiles(pathname -> !pathname.isDirectory());
|
||||||
|
if(files != null) {
|
||||||
|
for(File file : files) {
|
||||||
|
file.setExecutable(true, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set jspawnhelper executable
|
||||||
|
new File(jreLib, "jspawnhelper" + (SystemUtilities.isWindows() ? ".exe" : "")).setExecutable(true, false);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setExecutable(String relativePath) {
|
||||||
|
new File(getDestination(), relativePath).setExecutable(true, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Explicitly purge libs to notify system cache per https://github.com/qzind/tray/issues/662
|
||||||
|
*/
|
||||||
|
public Installer removeLibs() {
|
||||||
|
String[] dirs = { "libs" };
|
||||||
|
for (String dir : dirs) {
|
||||||
|
try {
|
||||||
|
FileUtils.deleteDirectory(new File(instance.getDestination() + File.separator + dir));
|
||||||
|
} catch(IOException ignore) {}
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Installer cleanupLegacyLogs(int rolloverCount) {
|
||||||
|
// Convert old < 2.2.3 log file format
|
||||||
|
Path logLocation = USER_DIR;
|
||||||
|
int oldIndex = 0;
|
||||||
|
int newIndex = 0;
|
||||||
|
File oldFile;
|
||||||
|
do {
|
||||||
|
// Old: debug.log.1
|
||||||
|
oldFile = logLocation.resolve("debug.log." + ++oldIndex).toFile();
|
||||||
|
if(oldFile.exists()) {
|
||||||
|
// New: debug.1.log
|
||||||
|
File newFile;
|
||||||
|
do {
|
||||||
|
newFile = logLocation.resolve("debug." + ++newIndex + ".log").toFile();
|
||||||
|
} while(newFile.exists());
|
||||||
|
|
||||||
|
oldFile.renameTo(newFile);
|
||||||
|
log.info("Migrated log file {} to new location {}", oldFile, newFile);
|
||||||
|
}
|
||||||
|
} while(oldFile.exists() || oldIndex <= rolloverCount);
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Installer removeLegacyFiles() {
|
||||||
|
ArrayList<String> dirs = new ArrayList<>();
|
||||||
|
ArrayList<String> files = new ArrayList<>();
|
||||||
|
HashMap<String, String> move = new HashMap<>();
|
||||||
|
|
||||||
|
// QZ Tray 2.0 files
|
||||||
|
dirs.add("demo/js/3rdparty");
|
||||||
|
dirs.add("utils");
|
||||||
|
dirs.add("auth");
|
||||||
|
files.add("demo/js/qz-websocket.js");
|
||||||
|
files.add("windows-icon.ico");
|
||||||
|
|
||||||
|
// QZ Tray 2.2.3-SNAPSHOT accidentally wrote certs in the wrong place
|
||||||
|
dirs.add("ssl");
|
||||||
|
|
||||||
|
// QZ Tray 2.1 files
|
||||||
|
if(SystemUtilities.isMac()) {
|
||||||
|
// Moved to macOS Application Bundle standard https://developer.apple.com/go/?id=bundle-structure
|
||||||
|
dirs.add("demo");
|
||||||
|
dirs.add("libs");
|
||||||
|
files.add(PROPS_FILE + ".jar");
|
||||||
|
files.add("LICENSE.txt");
|
||||||
|
files.add("uninstall");
|
||||||
|
move.put(PROPS_FILE + ".properties", "Contents/Resources/" + PROPS_FILE + ".properties");
|
||||||
|
}
|
||||||
|
|
||||||
|
dirs.forEach(dir -> {
|
||||||
|
try {
|
||||||
|
FileUtils.deleteDirectory(new File(instance.getDestination() + File.separator + dir));
|
||||||
|
} catch(IOException ignore) {}
|
||||||
|
});
|
||||||
|
|
||||||
|
files.forEach(file -> {
|
||||||
|
new File(instance.getDestination() + File.separator + file).delete();
|
||||||
|
});
|
||||||
|
|
||||||
|
move.forEach((src, dest) -> {
|
||||||
|
try {
|
||||||
|
FileUtils.moveFile(new File(instance.getDestination() + File.separator + src),
|
||||||
|
new File(instance.getDestination() + File.separator + dest));
|
||||||
|
} catch(IOException ignore) {}
|
||||||
|
});
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Installer addSharedDirectory() {
|
||||||
|
try {
|
||||||
|
Files.createDirectories(SHARED_DIR);
|
||||||
|
FileUtilities.setPermissionsRecursively(SHARED_DIR, true);
|
||||||
|
Path ssl = Paths.get(SHARED_DIR.toString(), "ssl");
|
||||||
|
Files.createDirectories(ssl);
|
||||||
|
FileUtilities.setPermissionsRecursively(ssl, true);
|
||||||
|
|
||||||
|
log.info("Created shared directory: {}", SHARED_DIR);
|
||||||
|
} catch(IOException e) {
|
||||||
|
log.warn("Could not create shared directory: {}", SHARED_DIR);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Installer removeSharedDirectory() {
|
||||||
|
try {
|
||||||
|
FileUtils.deleteDirectory(SHARED_DIR.toFile());
|
||||||
|
log.info("Deleted shared directory: {}", SHARED_DIR);
|
||||||
|
} catch(IOException e) {
|
||||||
|
log.warn("Could not delete shared directory: {}", SHARED_DIR);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks, and if needed generates an SSL for the system
|
||||||
|
*/
|
||||||
|
public CertificateManager certGen(boolean forceNew, String... hostNames) throws Exception {
|
||||||
|
CertificateManager certificateManager = new CertificateManager(forceNew, hostNames);
|
||||||
|
boolean needsInstall = certificateManager.needsInstall();
|
||||||
|
try {
|
||||||
|
// Check that the CA cert is installed
|
||||||
|
X509Certificate caCert = certificateManager.getKeyPair(CA).getCert();
|
||||||
|
NativeCertificateInstaller installer = NativeCertificateInstaller.getInstance();
|
||||||
|
|
||||||
|
if (forceNew || needsInstall) {
|
||||||
|
// Remove installed certs per request (usually the desktop installer, or failure to write properties)
|
||||||
|
// Skip if running from IDE, this may accidentally remove sandboxed certs
|
||||||
|
if(SystemUtilities.isJar()) {
|
||||||
|
List<String> matchingCerts = installer.find();
|
||||||
|
installer.remove(matchingCerts);
|
||||||
|
}
|
||||||
|
installer.install(caCert);
|
||||||
|
FirefoxCertificateInstaller.install(caCert, hostNames);
|
||||||
|
} else {
|
||||||
|
// Make sure the certificate is recognized by the system
|
||||||
|
if(caCert == null) {
|
||||||
|
log.info("CA cert is empty, skipping installation checks. This is normal for trusted/3rd-party SSL certificates.");
|
||||||
|
} else {
|
||||||
|
File tempCert = File.createTempFile(KeyPairWrapper.getAlias(KeyPairWrapper.Type.CA) + "-", CertificateManager.DEFAULT_CERTIFICATE_EXTENSION);
|
||||||
|
CertificateManager.writeCert(caCert, tempCert); // temp cert
|
||||||
|
if (!installer.verify(tempCert)) {
|
||||||
|
installer.install(caCert);
|
||||||
|
FirefoxCertificateInstaller.install(caCert, hostNames);
|
||||||
|
}
|
||||||
|
if(!tempCert.delete()) {
|
||||||
|
tempCert.deleteOnExit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(Exception e) {
|
||||||
|
log.error("Something went wrong obtaining the certificate. HTTPS will fail.", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add provisioning steps that come after certgen
|
||||||
|
if(SystemUtilities.isAdmin()) {
|
||||||
|
invokeProvisioning(Phase.CERTGEN);
|
||||||
|
}
|
||||||
|
|
||||||
|
return certificateManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove matching certs from user|system, then Firefox
|
||||||
|
*/
|
||||||
|
public Installer removeCerts() {
|
||||||
|
// System certs
|
||||||
|
NativeCertificateInstaller instance = NativeCertificateInstaller.getInstance();
|
||||||
|
instance.remove(instance.find());
|
||||||
|
// Firefox certs
|
||||||
|
FirefoxCertificateInstaller.uninstall();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add user-specific settings
|
||||||
|
* Note: See override usage for platform-specific tasks
|
||||||
|
*/
|
||||||
|
public Installer addUserSettings() {
|
||||||
|
// Check for whitelisted certificates in <install>/whitelist/
|
||||||
|
Path whiteList = SystemUtilities.getJarParentPath().resolve(WHITELIST_CERT_DIR);
|
||||||
|
if(Files.exists(whiteList) && Files.isDirectory(whiteList)) {
|
||||||
|
for(File file : whiteList.toFile().listFiles()) {
|
||||||
|
try {
|
||||||
|
Certificate cert = new Certificate(FileUtilities.readLocalFile(file.getPath()));
|
||||||
|
if (!cert.isSaved()) {
|
||||||
|
FileUtilities.addToCertList(ALLOW_FILE, file);
|
||||||
|
}
|
||||||
|
} catch(Exception e) {
|
||||||
|
log.warn("Could not add {} to {}", file, ALLOW_FILE, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Installer invokeProvisioning(Phase phase) {
|
||||||
|
try {
|
||||||
|
Path provisionPath = SystemUtilities.isMac() ?
|
||||||
|
Paths.get(getDestination()).resolve("Contents/Resources").resolve(PROVISION_DIR) :
|
||||||
|
Paths.get(getDestination()).resolve(PROVISION_DIR);
|
||||||
|
ProvisionInstaller provisionInstaller = new ProvisionInstaller(provisionPath);
|
||||||
|
provisionInstaller.invoke(phase);
|
||||||
|
|
||||||
|
// Special case for custom websocket ports
|
||||||
|
if(phase == Phase.INSTALL) {
|
||||||
|
websocketPorts = WebsocketPorts.parseFromSteps(provisionInstaller.getSteps());
|
||||||
|
}
|
||||||
|
} catch(Exception e) {
|
||||||
|
log.warn("An error occurred invoking provision \"phase\": \"{}\"", phase, e);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Installer removeProvisioning() {
|
||||||
|
try {
|
||||||
|
Path provisionPath = SystemUtilities.isMac() ?
|
||||||
|
Paths.get(getDestination()).resolve("Contents/Resources").resolve(PROVISION_DIR) :
|
||||||
|
Paths.get(getDestination()).resolve(PROVISION_DIR);
|
||||||
|
FileUtils.deleteDirectory(provisionPath.toFile());
|
||||||
|
} catch(Exception e) {
|
||||||
|
log.warn("An error occurred removing provision directory", e);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Properties persistProperties(File oldFile, Properties newProps) {
|
||||||
|
if(oldFile.exists()) {
|
||||||
|
Properties oldProps = new Properties();
|
||||||
|
try(Reader reader = new FileReader(oldFile)) {
|
||||||
|
oldProps.load(reader);
|
||||||
|
for(String key : PERSIST_PROPS) {
|
||||||
|
if (oldProps.containsKey(key)) {
|
||||||
|
String value = oldProps.getProperty(key);
|
||||||
|
log.info("Preserving {}={} for install", key, value);
|
||||||
|
newProps.put(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch(IOException e) {
|
||||||
|
log.warn("Warning, an error occurred reading the old properties file {}", oldFile, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return newProps;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void spawn(String ... args) throws Exception {
|
||||||
|
spawn(new ArrayList(Arrays.asList(args)));
|
||||||
|
}
|
||||||
|
}
|
||||||
371
old code/tray/src/qz/installer/LinuxInstaller.java
Executable file
371
old code/tray/src/qz/installer/LinuxInstaller.java
Executable file
@@ -0,0 +1,371 @@
|
|||||||
|
package qz.installer;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
import qz.utils.FileUtilities;
|
||||||
|
import qz.utils.ShellUtilities;
|
||||||
|
import qz.utils.SystemUtilities;
|
||||||
|
import qz.utils.UnixUtilities;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import static qz.common.Constants.*;
|
||||||
|
|
||||||
|
public class LinuxInstaller extends Installer {
|
||||||
|
protected static final Logger log = LogManager.getLogger(LinuxInstaller.class);
|
||||||
|
|
||||||
|
public static final String SHORTCUT_NAME = PROPS_FILE + ".desktop";
|
||||||
|
public static final String STARTUP_DIR = "/etc/xdg/autostart/";
|
||||||
|
public static final String STARTUP_LAUNCHER = STARTUP_DIR + SHORTCUT_NAME;
|
||||||
|
public static final String APP_DIR = "/usr/share/applications/";
|
||||||
|
public static final String APP_LAUNCHER = APP_DIR + SHORTCUT_NAME;
|
||||||
|
public static final String UDEV_RULES = "/lib/udev/rules.d/99-udev-override.rules";
|
||||||
|
public static final String[] CHROME_POLICY_DIRS = {"/etc/chromium/policies/managed", "/etc/opt/chrome/policies/managed" };
|
||||||
|
public static final String CHROME_POLICY = "{ \"URLAllowlist\": [\"" + DATA_DIR + "://*\"] }";
|
||||||
|
|
||||||
|
private String destination = "/opt/" + PROPS_FILE;
|
||||||
|
private String sudoer;
|
||||||
|
|
||||||
|
public LinuxInstaller() {
|
||||||
|
super();
|
||||||
|
sudoer = getSudoer();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDestination(String destination) {
|
||||||
|
this.destination = destination;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDestination() {
|
||||||
|
return destination;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Installer addAppLauncher() {
|
||||||
|
addLauncher(APP_LAUNCHER, false);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Installer addStartupEntry() {
|
||||||
|
addLauncher(STARTUP_LAUNCHER, true);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addLauncher(String location, boolean isStartup) {
|
||||||
|
HashMap<String, String> fieldMap = new HashMap<>();
|
||||||
|
// Dynamic fields
|
||||||
|
fieldMap.put("%DESTINATION%", destination);
|
||||||
|
fieldMap.put("%LINUX_ICON%", String.format("%s.svg", PROPS_FILE));
|
||||||
|
fieldMap.put("%COMMAND%", String.format("%s/%s", destination, PROPS_FILE));
|
||||||
|
fieldMap.put("%PARAM%", isStartup ? "--honorautostart" : "%u");
|
||||||
|
|
||||||
|
File launcher = new File(location);
|
||||||
|
try {
|
||||||
|
FileUtilities.configureAssetFile("assets/linux-shortcut.desktop.in", launcher, fieldMap, LinuxInstaller.class);
|
||||||
|
launcher.setReadable(true, false);
|
||||||
|
launcher.setExecutable(true, false);
|
||||||
|
} catch(IOException e) {
|
||||||
|
log.warn("Unable to write {} file: {}", isStartup ? "startup":"launcher", location, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Installer removeLegacyStartup() {
|
||||||
|
log.info("Removing legacy autostart entries for all users matching {} or {}", ABOUT_TITLE, PROPS_FILE);
|
||||||
|
// assume users are in /home
|
||||||
|
String[] shortcutNames = {ABOUT_TITLE, PROPS_FILE};
|
||||||
|
for(File file : new File("/home").listFiles()) {
|
||||||
|
if (file.isDirectory()) {
|
||||||
|
File userStart = new File(file.getPath() + "/.config/autostart");
|
||||||
|
if (userStart.exists() && userStart.isDirectory()) {
|
||||||
|
for (String shortcutName : shortcutNames) {
|
||||||
|
File legacyStartup = new File(userStart.getPath() + File.separator + shortcutName + ".desktop");
|
||||||
|
if(legacyStartup.exists()) {
|
||||||
|
legacyStartup.delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Installer addSystemSettings() {
|
||||||
|
// Legacy Ubuntu versions only: Patch Unity to show the System Tray
|
||||||
|
if(UnixUtilities.isUbuntu()) {
|
||||||
|
ShellUtilities.execute("gsettings", "set", "com.canonical.Unity.Panel", "systray", "-whitelist", "\"['all']\"");
|
||||||
|
|
||||||
|
if(ShellUtilities.execute("killall", "-w", "unity", "-panel")) {
|
||||||
|
ShellUtilities.execute("nohup", "unity", "-panel");
|
||||||
|
}
|
||||||
|
|
||||||
|
if(ShellUtilities.execute("killall", "-w", "unity", "-2d")) {
|
||||||
|
ShellUtilities.execute("nohup", "unity", "-2d");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chrome protocol handler
|
||||||
|
for (String policyDir : CHROME_POLICY_DIRS) {
|
||||||
|
log.info("Installing chrome protocol handler {}/{}...", policyDir, PROPS_FILE + ".json");
|
||||||
|
try {
|
||||||
|
FileUtilities.setPermissionsParentally(Files.createDirectories(Paths.get(policyDir)), false);
|
||||||
|
} catch(IOException e) {
|
||||||
|
log.warn("An error occurred creating {}", policyDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
Path policy = Paths.get(policyDir, PROPS_FILE + ".json");
|
||||||
|
try (BufferedWriter writer = new BufferedWriter(new FileWriter(policy.toFile()))){
|
||||||
|
writer.write(CHROME_POLICY);
|
||||||
|
policy.toFile().setReadable(true, false);
|
||||||
|
}
|
||||||
|
catch(IOException e) {
|
||||||
|
log.warn("Unable to write chrome policy: {} ({}:launch will fail)", policy, DATA_DIR);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// USB permissions
|
||||||
|
try {
|
||||||
|
File udev = new File(UDEV_RULES);
|
||||||
|
if (udev.exists()) {
|
||||||
|
udev.delete();
|
||||||
|
}
|
||||||
|
FileUtilities.configureAssetFile("assets/linux-udev.rules.in", new File(UDEV_RULES), new HashMap<>(), LinuxInstaller.class);
|
||||||
|
// udev rules should be -rw-r--r--
|
||||||
|
udev.setReadable(true, false);
|
||||||
|
ShellUtilities.execute("udevadm", "control", "--reload-rules");
|
||||||
|
} catch(IOException e) {
|
||||||
|
log.warn("Could not install udev rules, usb support may fail {}", UDEV_RULES, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup incorrectly placed files
|
||||||
|
File badFirefoxJs = new File("/usr/bin/defaults/pref/" + PROPS_FILE + ".js");
|
||||||
|
File badFirefoxCfg = new File("/usr/bin/" + PROPS_FILE + ".cfg");
|
||||||
|
|
||||||
|
if(badFirefoxCfg.exists()) {
|
||||||
|
log.info("Removing incorrectly placed Firefox configuration {}, {}...", badFirefoxJs, badFirefoxCfg);
|
||||||
|
badFirefoxCfg.delete();
|
||||||
|
new File("/usr/bin/defaults").delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup incorrectly placed files
|
||||||
|
File badFirefoxPolicy = new File("/usr/bin/distribution/policies.json");
|
||||||
|
if(badFirefoxPolicy.exists()) {
|
||||||
|
log.info("Removing incorrectly placed Firefox policy {}", badFirefoxPolicy);
|
||||||
|
badFirefoxPolicy.delete();
|
||||||
|
// Delete the distribution folder too, as long as it's empty
|
||||||
|
File badPolicyFolder = badFirefoxPolicy.getParentFile();
|
||||||
|
if(badPolicyFolder.isDirectory() && badPolicyFolder.listFiles().length == 0) {
|
||||||
|
badPolicyFolder.delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
log.info("Cleaning up any remaining files...");
|
||||||
|
new File(destination + File.separator + "install").delete();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Installer removeSystemSettings() {
|
||||||
|
// Chrome protocol handler
|
||||||
|
for (String policyDir : CHROME_POLICY_DIRS) {
|
||||||
|
log.info("Removing chrome protocol handler {}/{}...", policyDir, PROPS_FILE + ".json");
|
||||||
|
Path policy = Paths.get(policyDir, PROPS_FILE + ".json");
|
||||||
|
policy.toFile().delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
// USB permissions
|
||||||
|
File udev = new File(UDEV_RULES);
|
||||||
|
if (udev.exists()) {
|
||||||
|
udev.delete();
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Environmental variables for spawning a task using sudo. Order is important.
|
||||||
|
static String[] SUDO_EXPORTS = {"USER", "HOME", "UPSTART_SESSION", "DISPLAY", "DBUS_SESSION_BUS_ADDRESS", "XDG_CURRENT_DESKTOP", "GNOME_DESKTOP_SESSION_ID" };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Spawns the process as the underlying regular user account, preserving the environment
|
||||||
|
*/
|
||||||
|
public void spawn(List<String> args) throws Exception {
|
||||||
|
if(!SystemUtilities.isAdmin()) {
|
||||||
|
// Not admin, just run as the existing user
|
||||||
|
ShellUtilities.execute(args.toArray(new String[args.size()]));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get user's environment from dbus, etc
|
||||||
|
HashMap<String, String> env = getUserEnv(sudoer);
|
||||||
|
if(env.size() == 0) {
|
||||||
|
throw new Exception("Unable to get dbus info; can't spawn instance");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare the environment
|
||||||
|
String[] envp = new String[env.size() + ShellUtilities.envp.length];
|
||||||
|
int i = 0;
|
||||||
|
// Keep existing env
|
||||||
|
for(String keep : ShellUtilities.envp) {
|
||||||
|
envp[i++] = keep;
|
||||||
|
}
|
||||||
|
for(String key :env.keySet()) {
|
||||||
|
envp[i++] = String.format("%s=%s", key, env.get(key));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Concat "sudo|su", sudoer, "nohup", args
|
||||||
|
ArrayList<String> argsList = sudoCommand(sudoer, true, args);
|
||||||
|
|
||||||
|
// Spawn
|
||||||
|
log.info("Executing: {}", Arrays.toString(argsList.toArray()));
|
||||||
|
Runtime.getRuntime().exec(argsList.toArray(new String[argsList.size()]), envp);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a command to help running as another user using "sudo" or "su"
|
||||||
|
*/
|
||||||
|
public static ArrayList<String> sudoCommand(String sudoer, boolean async, List<String> cmds) {
|
||||||
|
ArrayList<String> sudo = new ArrayList<>();
|
||||||
|
if(StringUtils.isEmpty(sudoer) || !userExists(sudoer)) {
|
||||||
|
throw new UnsupportedOperationException(String.format("Parameter [sudoer: %s] is empty or the provided user was not found", sudoer));
|
||||||
|
}
|
||||||
|
if(ShellUtilities.execute("which", "sudo") // check if sudo exists
|
||||||
|
|| ShellUtilities.execute("sudo", "-u", sudoer, "-v")) { // check if user can login
|
||||||
|
// Pass directly into "sudo"
|
||||||
|
log.info("Guessing that this system prefers \"sudo\" over \"su\".");
|
||||||
|
sudo.add("sudo");
|
||||||
|
|
||||||
|
// Add calling user
|
||||||
|
sudo.add("-E"); // preserve environment
|
||||||
|
sudo.add("-u");
|
||||||
|
sudo.add(sudoer);
|
||||||
|
|
||||||
|
// Add "background" task support
|
||||||
|
if(async) {
|
||||||
|
sudo.add("nohup");
|
||||||
|
}
|
||||||
|
if(cmds != null && cmds.size() > 0) {
|
||||||
|
// Add additional commands
|
||||||
|
sudo.addAll(cmds);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Build and escape for "su"
|
||||||
|
log.info("Guessing that this system prefers \"su\" over \"sudo\".");
|
||||||
|
sudo.add("su");
|
||||||
|
|
||||||
|
// Add calling user
|
||||||
|
sudo.add(sudoer);
|
||||||
|
|
||||||
|
sudo.add("-c");
|
||||||
|
|
||||||
|
// Add "background" task support
|
||||||
|
if(async) {
|
||||||
|
sudo.add("nohup");
|
||||||
|
}
|
||||||
|
if(cmds != null && cmds.size() > 0) {
|
||||||
|
// Add additional commands
|
||||||
|
sudo.addAll(Arrays.asList(StringUtils.join(cmds, "\" \"") + "\""));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sudo;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the most likely non-root user account that the installer is running from
|
||||||
|
*/
|
||||||
|
private static String getSudoer() {
|
||||||
|
String sudoer = ShellUtilities.executeRaw("logname").trim();
|
||||||
|
if(sudoer.isEmpty() || SystemUtilities.isSolaris()) {
|
||||||
|
sudoer = System.getenv("SUDO_USER");
|
||||||
|
}
|
||||||
|
return sudoer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uses two common POSIX techniques for testing if the provided user account exists
|
||||||
|
*/
|
||||||
|
private static boolean userExists(String user) {
|
||||||
|
return ShellUtilities.execute("id", "-u", user) ||
|
||||||
|
ShellUtilities.execute("getent", "passwd", user);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to extract user environment variables from the dbus process to
|
||||||
|
* allow starting a graphical application as the current user.
|
||||||
|
*
|
||||||
|
* If this fails, items such as the user's desktop theme may not be known to Java
|
||||||
|
* at runtime resulting in the Swing L&F instead of the Gtk L&F.
|
||||||
|
*/
|
||||||
|
private static HashMap<String, String> getUserEnv(String matchingUser) {
|
||||||
|
if(!SystemUtilities.isAdmin()) {
|
||||||
|
throw new UnsupportedOperationException("Administrative access is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
String[] dbusMatches = { "ibus-daemon.*--panel", "dbus-daemon.*--config-file="};
|
||||||
|
|
||||||
|
ArrayList<String> pids = new ArrayList<>();
|
||||||
|
for(String dbusMatch : dbusMatches) {
|
||||||
|
pids.addAll(Arrays.asList(ShellUtilities.executeRaw("pgrep", "-f", dbusMatch).split("\\r?\\n")));
|
||||||
|
}
|
||||||
|
|
||||||
|
HashMap<String, String> env = new HashMap<>();
|
||||||
|
HashMap<String, String> tempEnv = new HashMap<>();
|
||||||
|
ArrayList<String> toExport = new ArrayList<>(Arrays.asList(SUDO_EXPORTS));
|
||||||
|
for(String pid : pids) {
|
||||||
|
if(pid.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
String[] vars;
|
||||||
|
if(SystemUtilities.isSolaris()) {
|
||||||
|
// Use pargs -e $$ to get environment
|
||||||
|
log.info("Reading environment info from [pargs, -e, {}]", pid);
|
||||||
|
String pargs = ShellUtilities.executeRaw("pargs", "-e", pid);
|
||||||
|
vars = pargs.split("\\r?\\n");
|
||||||
|
String delim = "]: ";
|
||||||
|
for(int i = 0; i < vars.length; i++) {
|
||||||
|
if(vars[i].contains(delim)) {
|
||||||
|
vars[i] = vars[i].substring(vars[i].indexOf(delim) + delim.length()).trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Assume /proc/$$/environ
|
||||||
|
String environ = String.format("/proc/%s/environ", pid);
|
||||||
|
String delim = Pattern.compile("\0").pattern();
|
||||||
|
log.info("Reading environment info from {}", environ);
|
||||||
|
vars = new String(Files.readAllBytes(Paths.get(environ))).split(delim);
|
||||||
|
}
|
||||||
|
for(String var : vars) {
|
||||||
|
String[] parts = var.split("=", 2);
|
||||||
|
if(parts.length == 2) {
|
||||||
|
String key = parts[0].trim();
|
||||||
|
String val = parts[1].trim();
|
||||||
|
if(toExport.contains(key)) {
|
||||||
|
tempEnv.put(key, val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch(Exception e) {
|
||||||
|
log.warn("An unexpected error occurred obtaining dbus info", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only add vars for the current user
|
||||||
|
if(matchingUser.trim().equals(tempEnv.get("USER"))) {
|
||||||
|
env.putAll(tempEnv);
|
||||||
|
} else {
|
||||||
|
log.debug("Expected USER={} but got USER={}, skipping results for {}", matchingUser, tempEnv.get("USER"), pid);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use gtk theme
|
||||||
|
if(env.containsKey("XDG_CURRENT_DESKTOP") && !env.containsKey("GNOME_DESKTOP_SESSION_ID")) {
|
||||||
|
if(env.get("XDG_CURRENT_DESKTOP").toLowerCase(Locale.ENGLISH).contains("gnome")) {
|
||||||
|
env.put("GNOME_DESKTOP_SESSION_ID", "this-is-deprecated");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return env;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
125
old code/tray/src/qz/installer/MacInstaller.java
Executable file
125
old code/tray/src/qz/installer/MacInstaller.java
Executable file
@@ -0,0 +1,125 @@
|
|||||||
|
package qz.installer;
|
||||||
|
/**
|
||||||
|
* @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
|
||||||
|
*/
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
import qz.utils.FileUtilities;
|
||||||
|
import qz.utils.ShellUtilities;
|
||||||
|
import qz.utils.SystemUtilities;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static qz.common.Constants.*;
|
||||||
|
|
||||||
|
public class MacInstaller extends Installer {
|
||||||
|
protected static final Logger log = LogManager.getLogger(MacInstaller.class);
|
||||||
|
private static final String PACKAGE_NAME = getPackageName();
|
||||||
|
public static final String LAUNCH_AGENT_PATH = String.format("/Library/LaunchAgents/%s.plist", MacInstaller.PACKAGE_NAME);
|
||||||
|
private String destination = "/Applications/" + ABOUT_TITLE + ".app";
|
||||||
|
|
||||||
|
public Installer addAppLauncher() {
|
||||||
|
// not needed; registered when "QZ Tray.app" is copied
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Installer addStartupEntry() {
|
||||||
|
File dest = new File(LAUNCH_AGENT_PATH);
|
||||||
|
HashMap<String, String> fieldMap = new HashMap<>();
|
||||||
|
// Dynamic fields
|
||||||
|
fieldMap.put("%PACKAGE_NAME%", PACKAGE_NAME);
|
||||||
|
fieldMap.put("%COMMAND%", String.format("%s/Contents/MacOS/%s", destination, ABOUT_TITLE));
|
||||||
|
fieldMap.put("%PARAM%", "--honorautostart");
|
||||||
|
|
||||||
|
try {
|
||||||
|
FileUtilities.configureAssetFile("assets/mac-launchagent.plist.in", dest, fieldMap, MacInstaller.class);
|
||||||
|
// Disable service until reboot
|
||||||
|
if(SystemUtilities.isMac()) {
|
||||||
|
ShellUtilities.execute("/bin/launchctl", "unload", MacInstaller.LAUNCH_AGENT_PATH);
|
||||||
|
}
|
||||||
|
} catch(IOException e) {
|
||||||
|
log.warn("Unable to write startup file: {}", dest, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDestination(String destination) {
|
||||||
|
this.destination = destination;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDestination() {
|
||||||
|
return destination;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Installer addSystemSettings() {
|
||||||
|
// Chrome protocol handler
|
||||||
|
String plist = "/Library/Preferences/com.google.Chrome.plist";
|
||||||
|
if(ShellUtilities.execute(new String[] { "/usr/bin/defaults", "write", plist }, new String[] {DATA_DIR + "://*" }).isEmpty()) {
|
||||||
|
ShellUtilities.execute("/usr/bin/defaults", "write", plist, "URLAllowlist", "-array-add", DATA_DIR +"://*");
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
public Installer removeSystemSettings() {
|
||||||
|
// Remove startup entry
|
||||||
|
File dest = new File(LAUNCH_AGENT_PATH);
|
||||||
|
dest.delete();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes legacy (<= 2.0) startup entries
|
||||||
|
*/
|
||||||
|
public Installer removeLegacyStartup() {
|
||||||
|
log.info("Removing startup entries for all users matching " + ABOUT_TITLE);
|
||||||
|
String script = "tell application \"System Events\" to delete "
|
||||||
|
+ "every login item where name is \"" + ABOUT_TITLE + "\""
|
||||||
|
+ " or name is \"" + PROPS_FILE + ".jar\"";
|
||||||
|
|
||||||
|
// Run on background thread in case System Events is hung or slow to respond
|
||||||
|
final String finalScript = script;
|
||||||
|
new Thread(() -> {
|
||||||
|
ShellUtilities.executeAppleScript(finalScript);
|
||||||
|
}).run();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getPackageName() {
|
||||||
|
String packageName;
|
||||||
|
String[] parts = ABOUT_URL.split("\\W");
|
||||||
|
if (parts.length >= 2) {
|
||||||
|
// Parse io.qz.qz-print from Constants
|
||||||
|
packageName = String.format("%s.%s.%s", parts[parts.length - 1], parts[parts.length - 2], PROPS_FILE);
|
||||||
|
} else {
|
||||||
|
// Fallback on something sane
|
||||||
|
packageName = "local." + PROPS_FILE;
|
||||||
|
}
|
||||||
|
return packageName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void spawn(List<String> args) throws Exception {
|
||||||
|
if(SystemUtilities.isAdmin()) {
|
||||||
|
// macOS unconventionally uses "$USER" during its install process
|
||||||
|
String sudoer = System.getenv("USER");
|
||||||
|
if(sudoer == null || sudoer.isEmpty() || sudoer.equals("root")) {
|
||||||
|
// Fallback, should only fire via Terminal + sudo
|
||||||
|
sudoer = ShellUtilities.executeRaw("logname").trim();
|
||||||
|
}
|
||||||
|
// Start directly without waitFor(...), avoids deadlocking
|
||||||
|
Runtime.getRuntime().exec(new String[] { "su", sudoer, "-c", "\"" + StringUtils.join(args, "\" \"") + "\""});
|
||||||
|
} else {
|
||||||
|
Runtime.getRuntime().exec(args.toArray(new String[args.size()]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
227
old code/tray/src/qz/installer/TaskKiller.java
Executable file
227
old code/tray/src/qz/installer/TaskKiller.java
Executable file
@@ -0,0 +1,227 @@
|
|||||||
|
/**
|
||||||
|
* @author Tres Finocchiaro
|
||||||
|
*
|
||||||
|
* Copyright (C) 2021 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.installer;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
import qz.utils.ShellUtilities;
|
||||||
|
import qz.utils.SystemUtilities;
|
||||||
|
import qz.utils.WindowsUtilities;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.HashSet;
|
||||||
|
|
||||||
|
import static qz.common.Constants.PROPS_FILE;
|
||||||
|
|
||||||
|
public class TaskKiller {
|
||||||
|
protected static final Logger log = LogManager.getLogger(TaskKiller.class);
|
||||||
|
private static final String[] JAR_NAMES = {
|
||||||
|
PROPS_FILE + ".jar",
|
||||||
|
"qz.App", // v2.2.0...
|
||||||
|
"qz.ws.PrintSocketServer" // v2.0.0...v2.1.6
|
||||||
|
};
|
||||||
|
private static final String[] KILL_PID_CMD_POSIX = { "kill", "-9" };
|
||||||
|
private static final String[] KILL_PID_CMD_WIN32 = { "taskkill.exe", "/F", "/PID" };
|
||||||
|
private static final String[] KILL_PID_CMD = SystemUtilities.isWindows() ? KILL_PID_CMD_WIN32 : KILL_PID_CMD_POSIX;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kills all QZ Tray processes, being careful not to kill itself
|
||||||
|
*/
|
||||||
|
public static boolean killAll() {
|
||||||
|
boolean success = true;
|
||||||
|
|
||||||
|
// Disable service until reboot
|
||||||
|
if(SystemUtilities.isMac()) {
|
||||||
|
ShellUtilities.execute("/bin/launchctl", "unload", MacInstaller.LAUNCH_AGENT_PATH);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use jcmd to get all java processes
|
||||||
|
HashSet<Integer> pids = findPidsJcmd();
|
||||||
|
if(!SystemUtilities.isWindows()) {
|
||||||
|
// Fallback to pgrep, needed for macOS (See JDK-8319589, JDK-8197387)
|
||||||
|
pids.addAll(findPidsPgrep());
|
||||||
|
} else if(WindowsUtilities.isSystemAccount()) {
|
||||||
|
// Fallback to powershell, needed for Windows
|
||||||
|
pids.addAll(findPidsPwsh());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Careful not to kill ourselves ;)
|
||||||
|
pids.remove(SystemUtilities.getProcessId());
|
||||||
|
|
||||||
|
// Kill each PID
|
||||||
|
String[] killPid = new String[KILL_PID_CMD.length + 1];
|
||||||
|
System.arraycopy(KILL_PID_CMD, 0, killPid, 0, KILL_PID_CMD.length);
|
||||||
|
for (Integer pid : pids) {
|
||||||
|
killPid[killPid.length - 1] = pid.toString();
|
||||||
|
success = success && ShellUtilities.execute(killPid);
|
||||||
|
}
|
||||||
|
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Path getJcmdPath() throws IOException {
|
||||||
|
Path jcmd;
|
||||||
|
if(SystemUtilities.isWindows()) {
|
||||||
|
jcmd = SystemUtilities.getJarParentPath().resolve("runtime/bin/jcmd.exe");
|
||||||
|
} else if (SystemUtilities.isMac()) {
|
||||||
|
jcmd = SystemUtilities.getJarParentPath().resolve("../PlugIns/Java.runtime/Contents/Home/bin/jcmd");
|
||||||
|
} else {
|
||||||
|
jcmd = SystemUtilities.getJarParentPath().resolve("runtime/bin/jcmd");
|
||||||
|
}
|
||||||
|
if(!jcmd.toFile().exists()) {
|
||||||
|
log.error("Could not find {}", jcmd);
|
||||||
|
throw new IOException("Could not find jcmd, we can't use it for detecting running instances");
|
||||||
|
}
|
||||||
|
return jcmd;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static final String[] PWSH_QUERY = { "powershell.exe", "-Command", "\"(Get-CimInstance Win32_Process -Filter \\\"Name = 'java.exe' OR Name = 'javaw.exe'\\\").Where({$_.CommandLine -like '*%s*'}).ProcessId\"" };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Leverage powershell.exe when run as SYSTEM to workaround https://github.com/qzind/tray/issues/1360
|
||||||
|
* TODO: Remove when jcmd is patched to work as SYSTEM account
|
||||||
|
*/
|
||||||
|
private static HashSet<Integer> findPidsPwsh() {
|
||||||
|
HashSet<Integer> foundPids = new HashSet<>();
|
||||||
|
|
||||||
|
for(String jarName : JAR_NAMES) {
|
||||||
|
String[] pwshQuery = PWSH_QUERY.clone();
|
||||||
|
int lastIndex = pwshQuery.length - 1;
|
||||||
|
// Format the last element to contain the jarName
|
||||||
|
pwshQuery[lastIndex] = String.format(pwshQuery[lastIndex], jarName);
|
||||||
|
String stdout = ShellUtilities.executeRaw(pwshQuery);
|
||||||
|
String[] lines = stdout.split("\\s*\\r?\\n");
|
||||||
|
for(String line : lines) {
|
||||||
|
if(line.trim().isEmpty()) {
|
||||||
|
// Don't try to process blank lines
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
int pid = parsePid(line);
|
||||||
|
if (pid >= 0) {
|
||||||
|
foundPids.add(pid);
|
||||||
|
} else {
|
||||||
|
log.warn("Could not parse PID value. Full line: '{}', Full output: '{}'", line, stdout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return foundPids;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use pgrep to fetch all PIDs to workaround https://github.com/openjdk/jdk/pull/25824
|
||||||
|
* TODO: Remove when jcmd is patched to work properly on macOS
|
||||||
|
*/
|
||||||
|
private static HashSet<Integer> findPidsPgrep() {
|
||||||
|
HashSet<Integer> foundPids = new HashSet<>();
|
||||||
|
|
||||||
|
for(String jarName : JAR_NAMES) {
|
||||||
|
String stdout = ShellUtilities.executeRaw("pgrep", "-f", jarName);
|
||||||
|
String[] lines = stdout.split("\\s*\\r?\\n");
|
||||||
|
for(String line : lines) {
|
||||||
|
if(line.trim().isEmpty()) {
|
||||||
|
// Don't try to process blank lines
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
int pid = parsePid(line);
|
||||||
|
if (pid >= 0) {
|
||||||
|
foundPids.add(pid);
|
||||||
|
} else {
|
||||||
|
log.warn("Could not parse PID value. Full line: '{}', Full output: '{}'", line, stdout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return foundPids;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uses jcmd to fetch all PIDs that match this product
|
||||||
|
*/
|
||||||
|
private static HashSet<Integer> findPidsJcmd() {
|
||||||
|
HashSet<Integer> foundPids = new HashSet<>();
|
||||||
|
|
||||||
|
String stdout;
|
||||||
|
String[] lines;
|
||||||
|
try {
|
||||||
|
stdout = ShellUtilities.executeRaw(getJcmdPath().toString(), "-l");
|
||||||
|
if(stdout == null) {
|
||||||
|
log.error("Error calling '{}' {}", getJcmdPath(), "-l");
|
||||||
|
return foundPids;
|
||||||
|
}
|
||||||
|
lines = stdout.split("\\r?\\n");
|
||||||
|
} catch(Exception e) {
|
||||||
|
log.error(e);
|
||||||
|
return foundPids;
|
||||||
|
}
|
||||||
|
|
||||||
|
for(String line : lines) {
|
||||||
|
if (line.trim().isEmpty()) {
|
||||||
|
// Don't try to process blank lines
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// e.g. "35446 C:\Program Files\QZ Tray\qz-tray.jar"
|
||||||
|
String[] parts = line.split(" ", 2);
|
||||||
|
int pid = parsePid(parts);
|
||||||
|
if (pid >= 0) {
|
||||||
|
String args = parseArgs(parts);
|
||||||
|
if (args == null) {
|
||||||
|
log.warn("Found PID value '{}' but no args to match. Full line: '{}', Full output: '{}'", pid, line, stdout);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for(String jarName : JAR_NAMES) {
|
||||||
|
if (args.contains(jarName)) {
|
||||||
|
foundPids.add(pid);
|
||||||
|
break; // continue parent loop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.warn("Could not parse PID value. Full line: '{}', Full output: '{}'", line, stdout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return foundPids;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the second index of a String[], trimmed
|
||||||
|
private static String parseArgs(String[] input) {
|
||||||
|
if(input != null) {
|
||||||
|
if(input.length == 2) {
|
||||||
|
return input[1].trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parses an int value form the first index of a String[], returning -1 if something went wrong
|
||||||
|
private static int parsePid(String[] input) {
|
||||||
|
if(input != null) {
|
||||||
|
if(input.length == 2) {
|
||||||
|
return parsePid(input[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parses an int value form the provided string, returning -1 if something went wrong
|
||||||
|
private static int parsePid(String input) {
|
||||||
|
String pidString = input.trim();
|
||||||
|
if(StringUtils.isNumeric(pidString)) {
|
||||||
|
return Integer.parseInt(pidString);
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
208
old code/tray/src/qz/installer/WindowsInstaller.java
Executable file
208
old code/tray/src/qz/installer/WindowsInstaller.java
Executable file
@@ -0,0 +1,208 @@
|
|||||||
|
/**
|
||||||
|
* @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 qz.installer;
|
||||||
|
|
||||||
|
import com.sun.jna.platform.win32.*;
|
||||||
|
import mslinks.ShellLink;
|
||||||
|
import mslinks.ShellLinkException;
|
||||||
|
import mslinks.ShellLinkHelper;
|
||||||
|
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.utils.ShellUtilities;
|
||||||
|
import qz.utils.SystemUtilities;
|
||||||
|
import qz.utils.WindowsUtilities;
|
||||||
|
import qz.ws.PrintSocketServer;
|
||||||
|
|
||||||
|
import javax.swing.*;
|
||||||
|
|
||||||
|
import static qz.common.Constants.*;
|
||||||
|
import static qz.installer.WindowsSpecialFolders.*;
|
||||||
|
import static com.sun.jna.platform.win32.WinReg.*;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.InvalidPathException;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
|
||||||
|
public class WindowsInstaller extends Installer {
|
||||||
|
protected static final Logger log = LogManager.getLogger(WindowsInstaller.class);
|
||||||
|
private String destination = getDefaultDestination();
|
||||||
|
private String destinationExe = getDefaultDestination() + File.separator + PROPS_FILE + ".exe";
|
||||||
|
|
||||||
|
public void setDestination(String destination) {
|
||||||
|
this.destination = destination;
|
||||||
|
this.destinationExe = destination + File.separator + PROPS_FILE + ".exe";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cycles through registry keys removing legacy (<= 2.0) startup entries
|
||||||
|
*/
|
||||||
|
public Installer removeLegacyStartup() {
|
||||||
|
log.info("Removing legacy startup entries for all users matching " + ABOUT_TITLE);
|
||||||
|
for (String user : Advapi32Util.registryGetKeys(HKEY_USERS)) {
|
||||||
|
WindowsUtilities.deleteRegValue(HKEY_USERS, user.trim() + "\\Software\\Microsoft\\Windows\\CurrentVersion\\Run", ABOUT_TITLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
FileUtils.deleteQuietly(new File(STARTUP + File.separator + ABOUT_TITLE + ".lnk"));
|
||||||
|
} catch(Win32Exception ignore) {}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Installer addAppLauncher() {
|
||||||
|
try {
|
||||||
|
// Delete old 2.0 launcher
|
||||||
|
FileUtils.deleteQuietly(new File(COMMON_START_MENU + File.separator + "Programs" + File.separator + ABOUT_TITLE + ".lnk"));
|
||||||
|
Path loc = Paths.get(COMMON_START_MENU.toString(), "Programs", ABOUT_TITLE);
|
||||||
|
loc.toFile().mkdirs();
|
||||||
|
String lnk = loc + File.separator + ABOUT_TITLE + ".lnk";
|
||||||
|
String exe = destination + File.separator + PROPS_FILE+ ".exe";
|
||||||
|
log.info("Creating launcher \"{}\" -> \"{}\"", lnk, exe);
|
||||||
|
ShellLinkHelper.createLink(exe, lnk);
|
||||||
|
} catch(ShellLinkException | IOException | Win32Exception e) {
|
||||||
|
log.warn("Could not create launcher", e);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Installer addStartupEntry() {
|
||||||
|
try {
|
||||||
|
String lnk = WindowsSpecialFolders.COMMON_STARTUP + File.separator + ABOUT_TITLE + ".lnk";
|
||||||
|
String exe = destination + File.separator + PROPS_FILE+ ".exe";
|
||||||
|
log.info("Creating startup entry \"{}\" -> \"{}\"", lnk, exe);
|
||||||
|
ShellLink link = ShellLinkHelper.createLink(exe, lnk).getLink();
|
||||||
|
link.setCMDArgs("--honorautostart"); // honors auto-start preferences
|
||||||
|
} catch(ShellLinkException | IOException | Win32Exception e) {
|
||||||
|
log.warn("Could not create startup launcher", e);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
public Installer removeSystemSettings() {
|
||||||
|
// Cleanup registry
|
||||||
|
WindowsUtilities.deleteRegKey(HKEY_LOCAL_MACHINE, "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\" + ABOUT_TITLE);
|
||||||
|
WindowsUtilities.deleteRegKey(HKEY_LOCAL_MACHINE, "Software\\" + ABOUT_TITLE);
|
||||||
|
WindowsUtilities.deleteRegKey(HKEY_LOCAL_MACHINE, DATA_DIR);
|
||||||
|
// Chrome protocol handler
|
||||||
|
WindowsUtilities.deleteRegData(HKEY_LOCAL_MACHINE, "SOFTWARE\\Policies\\Google\\Chrome\\URLAllowlist", String.format("%s://*", DATA_DIR));
|
||||||
|
// Deprecated Chrome protocol handler
|
||||||
|
WindowsUtilities.deleteRegData(HKEY_LOCAL_MACHINE, "SOFTWARE\\Policies\\Google\\Chrome\\URLWhitelist", String.format("%s://*", DATA_DIR));
|
||||||
|
|
||||||
|
// Cleanup launchers
|
||||||
|
for(WindowsSpecialFolders folder : new WindowsSpecialFolders[] { START_MENU, COMMON_START_MENU, DESKTOP, PUBLIC_DESKTOP, COMMON_STARTUP, RECENT }) {
|
||||||
|
try {
|
||||||
|
new File(folder + File.separator + ABOUT_TITLE + ".lnk").delete();
|
||||||
|
// Since 2.1, start menus use subfolder
|
||||||
|
if (folder.equals(COMMON_START_MENU) || folder.equals(START_MENU)) {
|
||||||
|
FileUtils.deleteQuietly(new File(folder + File.separator + "Programs" + File.separator + ABOUT_TITLE + ".lnk"));
|
||||||
|
FileUtils.deleteDirectory(new File(folder + File.separator + "Programs" + File.separator + ABOUT_TITLE));
|
||||||
|
}
|
||||||
|
} catch(InvalidPathException | IOException | Win32Exception ignore) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup firewall rules
|
||||||
|
ShellUtilities.execute("netsh.exe", "advfirewall", "firewall", "delete", "rule", String.format("name=%s", ABOUT_TITLE));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Installer addSystemSettings() {
|
||||||
|
/**
|
||||||
|
* TODO: Upgrade JNA!
|
||||||
|
* 64-bit registry view is currently invoked by nsis (windows-installer.nsi.in) using SetRegView 64
|
||||||
|
* However, newer version of JNA offer direct WinNT.KEY_WOW64_64KEY registry support, safeguarding
|
||||||
|
* against direct calls to "java -jar qz-tray.jar install|keygen|etc", which will be needed moving forward
|
||||||
|
* for support and troubleshooting.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Mime-type support e.g. qz:launch
|
||||||
|
WindowsUtilities.addRegValue(HKEY_CLASSES_ROOT, DATA_DIR, "", String.format("URL:%s Protocol", ABOUT_TITLE));
|
||||||
|
WindowsUtilities.addRegValue(HKEY_CLASSES_ROOT, DATA_DIR, "URL Protocol", "");
|
||||||
|
WindowsUtilities.addRegValue(HKEY_CLASSES_ROOT, String.format("%s\\DefaultIcon", DATA_DIR), "", String.format("\"%s\",1", destinationExe));
|
||||||
|
WindowsUtilities.addRegValue(HKEY_CLASSES_ROOT, String.format("%s\\shell\\open\\command", DATA_DIR), "", String.format("\"%s\" \"%%1\"", destinationExe));
|
||||||
|
|
||||||
|
/// Uninstall info
|
||||||
|
String uninstallKey = String.format("Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\%s", ABOUT_TITLE);
|
||||||
|
WindowsUtilities.addRegValue(HKEY_LOCAL_MACHINE, String.format("Software\\%s", ABOUT_TITLE), "", destination);
|
||||||
|
WindowsUtilities.addRegValue(HKEY_LOCAL_MACHINE, uninstallKey, "DisplayName", String.format("%s %s", ABOUT_TITLE, VERSION));
|
||||||
|
WindowsUtilities.addRegValue(HKEY_LOCAL_MACHINE, uninstallKey, "Publisher", ABOUT_COMPANY);
|
||||||
|
WindowsUtilities.addRegValue(HKEY_LOCAL_MACHINE, uninstallKey, "UninstallString", destination + File.separator + "uninstall.exe");
|
||||||
|
WindowsUtilities.addRegValue(HKEY_LOCAL_MACHINE, uninstallKey, "DisplayIcon", destinationExe);
|
||||||
|
WindowsUtilities.addRegValue(HKEY_LOCAL_MACHINE, uninstallKey, "HelpLink", ABOUT_SUPPORT_URL );
|
||||||
|
WindowsUtilities.addRegValue(HKEY_LOCAL_MACHINE, uninstallKey, "URLUpdateInfo", ABOUT_DOWNLOAD_URL);
|
||||||
|
WindowsUtilities.addRegValue(HKEY_LOCAL_MACHINE, uninstallKey, "URLInfoAbout", ABOUT_SUPPORT_URL);
|
||||||
|
WindowsUtilities.addRegValue(HKEY_LOCAL_MACHINE, uninstallKey, "DisplayVersion", VERSION.toString());
|
||||||
|
WindowsUtilities.addRegValue(HKEY_LOCAL_MACHINE, uninstallKey, "EstimatedSize", FileUtils.sizeOfDirectoryAsBigInteger(new File(destination)).intValue() / 1024);
|
||||||
|
|
||||||
|
// Chrome protocol handler
|
||||||
|
WindowsUtilities.addNumberedRegValue(HKEY_LOCAL_MACHINE, "SOFTWARE\\Policies\\Google\\Chrome\\URLAllowlist", String.format("%s://*", DATA_DIR));
|
||||||
|
|
||||||
|
// Firewall rules
|
||||||
|
ShellUtilities.execute("netsh.exe", "advfirewall", "firewall", "delete", "rule", String.format("name=%s", ABOUT_TITLE));
|
||||||
|
ShellUtilities.execute("netsh.exe", "advfirewall", "firewall", "add", "rule", String.format("name=%s", ABOUT_TITLE),
|
||||||
|
"dir=in", "action=allow", "profile=any", String.format("localport=%s", websocketPorts.allPortsAsString()), "localip=any", "protocol=tcp");
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Installer addUserSettings() {
|
||||||
|
// Whitelist loopback for IE/Edge
|
||||||
|
if(ShellUtilities.execute("CheckNetIsolation.exe", "LoopbackExempt", "-a", "-n=Microsoft.MicrosoftEdge_8wekyb3d8bbwe")) {
|
||||||
|
log.warn("Could not whitelist loopback connections for IE, Edge");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Intranet settings; uncheck "include sites not listed in other zones"
|
||||||
|
String key = "Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\\Zones\\1";
|
||||||
|
String value = "Flags";
|
||||||
|
if (Advapi32Util.registryKeyExists(HKEY_CURRENT_USER, key) && Advapi32Util.registryValueExists(HKEY_CURRENT_USER, key, value)) {
|
||||||
|
int data = Advapi32Util.registryGetIntValue(HKEY_CURRENT_USER, key, value);
|
||||||
|
// remove value using bitwise XOR
|
||||||
|
Advapi32Util.registrySetIntValue(HKEY_CURRENT_USER, key, value, data ^ 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Legacy Edge loopback support
|
||||||
|
key = "Software\\Classes\\Local Settings\\Software\\Microsoft\\Windows\\CurrentVersion\\AppContainer\\Storage\\microsoft.microsoftedge_8wekyb3d8bbwe\\MicrosoftEdge\\ExperimentalFeatures";
|
||||||
|
value = "AllowLocalhostLoopback";
|
||||||
|
if (Advapi32Util.registryKeyExists(HKEY_CURRENT_USER, key) && Advapi32Util.registryValueExists(HKEY_CURRENT_USER, key, value)) {
|
||||||
|
int data = Advapi32Util.registryGetIntValue(HKEY_CURRENT_USER, key, value);
|
||||||
|
// remove value using bitwise OR
|
||||||
|
Advapi32Util.registrySetIntValue(HKEY_CURRENT_USER, key, value, data | 1);
|
||||||
|
}
|
||||||
|
} catch(Exception e) {
|
||||||
|
log.warn("An error occurred configuring the \"Local Intranet Zone\"; connections to \"localhost\" may fail", e);
|
||||||
|
}
|
||||||
|
return super.addUserSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getDefaultDestination() {
|
||||||
|
String path = System.getenv("ProgramW6432");
|
||||||
|
if (path == null || path.trim().isEmpty()) {
|
||||||
|
path = System.getenv("ProgramFiles");
|
||||||
|
}
|
||||||
|
return path + File.separator + ABOUT_TITLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDestination() {
|
||||||
|
return destination;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void spawn(List<String> args) throws Exception {
|
||||||
|
if(SystemUtilities.isAdmin()) {
|
||||||
|
log.warn("Spawning as user isn't implemented; starting process with elevation instead");
|
||||||
|
}
|
||||||
|
ShellUtilities.execute(args.toArray(new String[args.size()]));
|
||||||
|
}
|
||||||
|
}
|
||||||
97
old code/tray/src/qz/installer/WindowsSpecialFolders.java
Executable file
97
old code/tray/src/qz/installer/WindowsSpecialFolders.java
Executable file
@@ -0,0 +1,97 @@
|
|||||||
|
/**
|
||||||
|
* @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.installer;
|
||||||
|
|
||||||
|
import com.sun.jna.platform.win32.*;
|
||||||
|
import qz.utils.WindowsUtilities;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Windows XP-compatible special folder's wrapper for JNA
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public enum WindowsSpecialFolders {
|
||||||
|
ADMIN_TOOLS(ShlObj.CSIDL_ADMINTOOLS, KnownFolders.FOLDERID_AdminTools),
|
||||||
|
STARTUP_ALT(ShlObj.CSIDL_ALTSTARTUP, KnownFolders.FOLDERID_Startup),
|
||||||
|
ROAMING_APPDATA(ShlObj.CSIDL_APPDATA, KnownFolders.FOLDERID_RoamingAppData),
|
||||||
|
RECYCLING_BIN(ShlObj.CSIDL_BITBUCKET, KnownFolders.FOLDERID_RecycleBinFolder),
|
||||||
|
CD_BURNING(ShlObj.CSIDL_CDBURN_AREA, KnownFolders.FOLDERID_CDBurning),
|
||||||
|
COMMON_ADMIN_TOOLS(ShlObj.CSIDL_COMMON_ADMINTOOLS, KnownFolders.FOLDERID_CommonAdminTools),
|
||||||
|
COMMON_STARTUP_ALT(ShlObj.CSIDL_COMMON_ALTSTARTUP, KnownFolders.FOLDERID_CommonStartup),
|
||||||
|
PROGRAM_DATA(ShlObj.CSIDL_COMMON_APPDATA, KnownFolders.FOLDERID_ProgramData),
|
||||||
|
PUBLIC_DESKTOP(ShlObj.CSIDL_COMMON_DESKTOPDIRECTORY, KnownFolders.FOLDERID_PublicDesktop),
|
||||||
|
PUBLIC_DOCUMENTS(ShlObj.CSIDL_COMMON_DOCUMENTS, KnownFolders.FOLDERID_PublicDocuments),
|
||||||
|
COMMON_FAVORITES(ShlObj.CSIDL_COMMON_FAVORITES, KnownFolders.FOLDERID_Favorites),
|
||||||
|
COMMON_MUSIC(ShlObj.CSIDL_COMMON_MUSIC, KnownFolders.FOLDERID_PublicMusic),
|
||||||
|
COMMON_OEM_LINKS(ShlObj.CSIDL_COMMON_OEM_LINKS, KnownFolders.FOLDERID_CommonOEMLinks),
|
||||||
|
COMMON_PICTURES(ShlObj.CSIDL_COMMON_PICTURES, KnownFolders.FOLDERID_PublicPictures),
|
||||||
|
COMMON_PROGRAMS(ShlObj.CSIDL_COMMON_PROGRAMS, KnownFolders.FOLDERID_CommonPrograms),
|
||||||
|
COMMON_START_MENU(ShlObj.CSIDL_COMMON_STARTMENU, KnownFolders.FOLDERID_CommonStartMenu),
|
||||||
|
COMMON_STARTUP(ShlObj.CSIDL_COMMON_STARTUP, KnownFolders.FOLDERID_CommonStartup),
|
||||||
|
COMMON_TEMPLATES(ShlObj.CSIDL_COMMON_TEMPLATES, KnownFolders.FOLDERID_CommonTemplates),
|
||||||
|
COMMON_VIDEO(ShlObj.CSIDL_COMMON_VIDEO, KnownFolders.FOLDERID_PublicVideos),
|
||||||
|
COMPUTERS_NEAR_ME(ShlObj.CSIDL_COMPUTERSNEARME, KnownFolders.FOLDERID_NetworkFolder),
|
||||||
|
CONNECTIONS_FOLDER(ShlObj.CSIDL_CONNECTIONS, KnownFolders.FOLDERID_ConnectionsFolder),
|
||||||
|
CONTROL_PANEL(ShlObj.CSIDL_CONTROLS, KnownFolders.FOLDERID_ControlPanelFolder),
|
||||||
|
COOKIES(ShlObj.CSIDL_COOKIES, KnownFolders.FOLDERID_Cookies),
|
||||||
|
DESKTOP_VIRTUAL(ShlObj.CSIDL_DESKTOP, KnownFolders.FOLDERID_Desktop),
|
||||||
|
DESKTOP(ShlObj.CSIDL_DESKTOPDIRECTORY, KnownFolders.FOLDERID_Desktop),
|
||||||
|
COMPUTER_FOLDER(ShlObj.CSIDL_DRIVES, KnownFolders.FOLDERID_ComputerFolder),
|
||||||
|
FAVORITES(ShlObj.CSIDL_FAVORITES, KnownFolders.FOLDERID_Favorites),
|
||||||
|
FONTS(ShlObj.CSIDL_FONTS, KnownFolders.FOLDERID_Fonts),
|
||||||
|
HISTORY(ShlObj.CSIDL_HISTORY, KnownFolders.FOLDERID_History),
|
||||||
|
INTERNET_FOLDER(ShlObj.CSIDL_INTERNET, KnownFolders.FOLDERID_InternetFolder),
|
||||||
|
INTERNET_CACHE(ShlObj.CSIDL_INTERNET_CACHE, KnownFolders.FOLDERID_InternetCache),
|
||||||
|
LOCAL_APPDATA(ShlObj.CSIDL_LOCAL_APPDATA, KnownFolders.FOLDERID_LocalAppData),
|
||||||
|
MY_DOCUMENTS(ShlObj.CSIDL_MYDOCUMENTS, KnownFolders.FOLDERID_Documents),
|
||||||
|
MY_MUSIC(ShlObj.CSIDL_MYMUSIC, KnownFolders.FOLDERID_Music),
|
||||||
|
MY_PICTURES(ShlObj.CSIDL_MYPICTURES, KnownFolders.FOLDERID_Pictures),
|
||||||
|
MY_VIDEOS(ShlObj.CSIDL_MYVIDEO, KnownFolders.FOLDERID_Videos),
|
||||||
|
NETWORK_NEIGHBORHOOD(ShlObj.CSIDL_NETHOOD, KnownFolders.FOLDERID_NetHood),
|
||||||
|
NETWORK_FOLDER(ShlObj.CSIDL_NETWORK, KnownFolders.FOLDERID_NetworkFolder),
|
||||||
|
PERSONAL_FOLDDER(ShlObj.CSIDL_PERSONAL, KnownFolders.FOLDERID_Documents),
|
||||||
|
PRINTERS(ShlObj.CSIDL_PRINTERS, KnownFolders.FOLDERID_PrintersFolder),
|
||||||
|
PRINTING_NEIGHBORHOODD(ShlObj.CSIDL_PRINTHOOD, KnownFolders.FOLDERID_PrintHood),
|
||||||
|
PROFILE_FOLDER(ShlObj.CSIDL_PROFILE, KnownFolders.FOLDERID_Profile),
|
||||||
|
PROGRAM_FILES(ShlObj.CSIDL_PROGRAM_FILES, KnownFolders.FOLDERID_ProgramFiles),
|
||||||
|
PROGRAM_FILESX86(ShlObj.CSIDL_PROGRAM_FILESX86, KnownFolders.FOLDERID_ProgramFilesX86),
|
||||||
|
PROGRAM_FILES_COMMON(ShlObj.CSIDL_PROGRAM_FILES_COMMON, KnownFolders.FOLDERID_ProgramFilesCommon),
|
||||||
|
PROGRAM_FILES_COMMONX86(ShlObj.CSIDL_PROGRAM_FILES_COMMONX86, KnownFolders.FOLDERID_ProgramFilesCommonX86),
|
||||||
|
PROGRAMS(ShlObj.CSIDL_PROGRAMS, KnownFolders.FOLDERID_Programs),
|
||||||
|
RECENT(ShlObj.CSIDL_RECENT, KnownFolders.FOLDERID_Recent),
|
||||||
|
RESOURCES(ShlObj.CSIDL_RESOURCES, KnownFolders.FOLDERID_ResourceDir),
|
||||||
|
RESOURCES_LOCALIZED(ShlObj.CSIDL_RESOURCES_LOCALIZED, KnownFolders.FOLDERID_LocalizedResourcesDir),
|
||||||
|
SEND_TO(ShlObj.CSIDL_SENDTO, KnownFolders.FOLDERID_SendTo),
|
||||||
|
START_MENU(ShlObj.CSIDL_STARTMENU, KnownFolders.FOLDERID_StartMenu),
|
||||||
|
STARTUP(ShlObj.CSIDL_STARTUP, KnownFolders.FOLDERID_Startup),
|
||||||
|
SYSTEM(ShlObj.CSIDL_SYSTEM, KnownFolders.FOLDERID_System),
|
||||||
|
SYSTEMX86(ShlObj.CSIDL_SYSTEMX86, KnownFolders.FOLDERID_SystemX86),
|
||||||
|
TEMPLATES(ShlObj.CSIDL_TEMPLATES, KnownFolders.FOLDERID_Templates),
|
||||||
|
WINDOWS(ShlObj.CSIDL_WINDOWS, KnownFolders.FOLDERID_Windows);
|
||||||
|
|
||||||
|
private int csidl;
|
||||||
|
private Guid.GUID guid;
|
||||||
|
WindowsSpecialFolders(int csidl, Guid.GUID guid) {
|
||||||
|
this.csidl = csidl;
|
||||||
|
this.guid = guid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPath() {
|
||||||
|
if(WindowsUtilities.isWindowsXP()) {
|
||||||
|
return Shell32Util.getSpecialFolderPath(csidl, false);
|
||||||
|
}
|
||||||
|
return Shell32Util.getKnownFolderPath(guid);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return getPath();
|
||||||
|
}
|
||||||
|
}
|
||||||
8
old code/tray/src/qz/installer/assets/linux-shortcut.desktop.in
Executable file
8
old code/tray/src/qz/installer/assets/linux-shortcut.desktop.in
Executable file
@@ -0,0 +1,8 @@
|
|||||||
|
[Desktop Entry]
|
||||||
|
Type=Application
|
||||||
|
Name=%ABOUT_TITLE%
|
||||||
|
Exec="%COMMAND%" %PARAM%
|
||||||
|
Path=%DESTINATION%
|
||||||
|
Icon=%DESTINATION%/%LINUX_ICON%
|
||||||
|
MimeType=application/x-qz;x-scheme-handler/qz;
|
||||||
|
Terminal=false
|
||||||
2
old code/tray/src/qz/installer/assets/linux-udev.rules.in
Executable file
2
old code/tray/src/qz/installer/assets/linux-udev.rules.in
Executable file
@@ -0,0 +1,2 @@
|
|||||||
|
# %ABOUT_TITLE% usb override settings
|
||||||
|
SUBSYSTEM=="usb", ENV{DEVTYPE}=="usb_device", MODE="0666"
|
||||||
18
old code/tray/src/qz/installer/assets/mac-launchagent.plist.in
Executable file
18
old code/tray/src/qz/installer/assets/mac-launchagent.plist.in
Executable file
@@ -0,0 +1,18 @@
|
|||||||
|
<?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>Label</key><string>%PACKAGE_NAME%</string>
|
||||||
|
<key>KeepAlive</key>
|
||||||
|
<dict>
|
||||||
|
<key>SuccessfulExit</key><false/>
|
||||||
|
<key>AfterInitialDemand</key><false/>
|
||||||
|
</dict>
|
||||||
|
<key>RunAtLoad</key><true/>
|
||||||
|
<key>ProgramArguments</key>
|
||||||
|
<array>
|
||||||
|
<string>%COMMAND%</string>
|
||||||
|
<string>%PARAM%</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
147
old code/tray/src/qz/installer/certificate/CertificateChainBuilder.java
Executable file
147
old code/tray/src/qz/installer/certificate/CertificateChainBuilder.java
Executable file
@@ -0,0 +1,147 @@
|
|||||||
|
/**
|
||||||
|
* @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 qz.installer.certificate;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.security.*;
|
||||||
|
import java.util.Calendar;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
import org.bouncycastle.asn1.*;
|
||||||
|
import org.bouncycastle.asn1.x500.X500Name;
|
||||||
|
import org.bouncycastle.asn1.x500.X500NameBuilder;
|
||||||
|
import org.bouncycastle.asn1.x500.style.BCStyle;
|
||||||
|
import org.bouncycastle.asn1.x509.*;
|
||||||
|
import org.bouncycastle.cert.X509CertificateHolder;
|
||||||
|
import org.bouncycastle.cert.X509v3CertificateBuilder;
|
||||||
|
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
|
||||||
|
import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils;
|
||||||
|
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
|
||||||
|
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||||
|
import org.bouncycastle.operator.ContentSigner;
|
||||||
|
import org.bouncycastle.operator.OperatorException;
|
||||||
|
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
|
||||||
|
import qz.common.Constants;
|
||||||
|
import qz.utils.SystemUtilities;
|
||||||
|
|
||||||
|
import static qz.installer.certificate.KeyPairWrapper.Type.*;
|
||||||
|
|
||||||
|
public class CertificateChainBuilder {
|
||||||
|
public static final String[] DEFAULT_HOSTNAMES = {"localhost", "localhost.qz.io" };
|
||||||
|
|
||||||
|
private static int KEY_SIZE = 2048;
|
||||||
|
public static int CA_CERT_AGE = 7305; // 20 years
|
||||||
|
public static int SSL_CERT_AGE = 825; // Per https://support.apple.com/HT210176
|
||||||
|
|
||||||
|
private String[] hostNames;
|
||||||
|
|
||||||
|
public CertificateChainBuilder(String ... hostNames) {
|
||||||
|
Security.addProvider(new BouncyCastleProvider());
|
||||||
|
if(hostNames.length > 0) {
|
||||||
|
this.hostNames = hostNames;
|
||||||
|
} else {
|
||||||
|
this.hostNames = DEFAULT_HOSTNAMES;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public KeyPairWrapper createCaCert() throws IOException, GeneralSecurityException, OperatorException {
|
||||||
|
KeyPair keyPair = createRsaKey();
|
||||||
|
|
||||||
|
X509v3CertificateBuilder builder = createX509Cert(keyPair, CA_CERT_AGE, hostNames);
|
||||||
|
|
||||||
|
builder.addExtension(Extension.basicConstraints, true, new BasicConstraints(1))
|
||||||
|
.addExtension(Extension.keyUsage, true, new KeyUsage(KeyUsage.keyCertSign + KeyUsage.cRLSign))
|
||||||
|
.addExtension(Extension.subjectKeyIdentifier, false, new JcaX509ExtensionUtils().createSubjectKeyIdentifier(keyPair.getPublic()));
|
||||||
|
|
||||||
|
// Signing
|
||||||
|
ContentSigner sign = new JcaContentSignerBuilder("SHA256withRSA").setProvider("BC").build(keyPair.getPrivate());
|
||||||
|
X509CertificateHolder certHolder = builder.build(sign);
|
||||||
|
|
||||||
|
// Convert to java-friendly format
|
||||||
|
return new KeyPairWrapper(CA, keyPair, new JcaX509CertificateConverter().setProvider("BC").getCertificate(certHolder));
|
||||||
|
}
|
||||||
|
|
||||||
|
public KeyPairWrapper createSslCert(KeyPairWrapper caKeyPairWrapper) throws IOException, GeneralSecurityException, OperatorException {
|
||||||
|
KeyPair sslKeyPair = createRsaKey();
|
||||||
|
X509v3CertificateBuilder builder = createX509Cert(sslKeyPair, SSL_CERT_AGE, hostNames);
|
||||||
|
|
||||||
|
JcaX509ExtensionUtils utils = new JcaX509ExtensionUtils();
|
||||||
|
|
||||||
|
builder.addExtension(Extension.authorityKeyIdentifier, false, utils.createAuthorityKeyIdentifier(caKeyPairWrapper.getCert()))
|
||||||
|
.addExtension(Extension.basicConstraints, true, new BasicConstraints(false))
|
||||||
|
.addExtension(Extension.keyUsage, true, new KeyUsage(KeyUsage.digitalSignature + KeyUsage.keyEncipherment))
|
||||||
|
.addExtension(Extension.extendedKeyUsage, false, new ExtendedKeyUsage(new KeyPurposeId[]{KeyPurposeId.id_kp_serverAuth, KeyPurposeId.id_kp_clientAuth}))
|
||||||
|
.addExtension(Extension.subjectAlternativeName, false, buildSan(hostNames))
|
||||||
|
.addExtension(Extension.subjectKeyIdentifier, false, utils.createSubjectKeyIdentifier(sslKeyPair.getPublic()));
|
||||||
|
|
||||||
|
// Signing
|
||||||
|
ContentSigner sign = new JcaContentSignerBuilder("SHA256withRSA").setProvider("BC").build(caKeyPairWrapper.getKey());
|
||||||
|
X509CertificateHolder certHolder = builder.build(sign);
|
||||||
|
|
||||||
|
// Convert to java-friendly format
|
||||||
|
return new KeyPairWrapper(SSL, sslKeyPair, new JcaX509CertificateConverter().setProvider("BC").getCertificate(certHolder));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static KeyPair createRsaKey() throws GeneralSecurityException {
|
||||||
|
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", "BC");
|
||||||
|
keyPairGenerator.initialize(KEY_SIZE, new SecureRandom());
|
||||||
|
return keyPairGenerator.generateKeyPair();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static X509v3CertificateBuilder createX509Cert(KeyPair keyPair, int age, String ... hostNames) {
|
||||||
|
String cn = hostNames.length > 0? hostNames[0]:DEFAULT_HOSTNAMES[0];
|
||||||
|
X500Name name = new X500NameBuilder()
|
||||||
|
.addRDN(BCStyle.C, Constants.ABOUT_COUNTRY)
|
||||||
|
.addRDN(BCStyle.ST, Constants.ABOUT_STATE)
|
||||||
|
.addRDN(BCStyle.L, Constants.ABOUT_CITY)
|
||||||
|
.addRDN(BCStyle.O, Constants.ABOUT_COMPANY)
|
||||||
|
.addRDN(BCStyle.OU, Constants.ABOUT_COMPANY)
|
||||||
|
.addRDN(BCStyle.EmailAddress, Constants.ABOUT_EMAIL)
|
||||||
|
.addRDN(BCStyle.CN, cn)
|
||||||
|
.build();
|
||||||
|
BigInteger serial = BigInteger.valueOf(System.currentTimeMillis());
|
||||||
|
Calendar notBefore = Calendar.getInstance(Locale.ENGLISH);
|
||||||
|
Calendar notAfter = Calendar.getInstance(Locale.ENGLISH);
|
||||||
|
notBefore.add(Calendar.DAY_OF_YEAR, -1);
|
||||||
|
notAfter.add(Calendar.DAY_OF_YEAR, age - 1);
|
||||||
|
|
||||||
|
SystemUtilities.swapLocale();
|
||||||
|
X509v3CertificateBuilder x509builder = new JcaX509v3CertificateBuilder(name, serial, notBefore.getTime(), notAfter.getTime(), name, keyPair.getPublic());
|
||||||
|
SystemUtilities.restoreLocale();
|
||||||
|
return x509builder;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds subjectAlternativeName extension; iterates and detects IPv4 or hostname
|
||||||
|
*/
|
||||||
|
private static GeneralNames buildSan(String ... hostNames) {
|
||||||
|
GeneralName[] gn = new GeneralName[hostNames.length];
|
||||||
|
for (int i = 0; i < hostNames.length; i++) {
|
||||||
|
int gnType = isIp(hostNames[i]) ? GeneralName.iPAddress : GeneralName.dNSName;
|
||||||
|
gn[i] = new GeneralName(gnType, hostNames[i]);
|
||||||
|
}
|
||||||
|
return GeneralNames.getInstance(new DERSequence(gn));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isIp(String ip) {
|
||||||
|
try {
|
||||||
|
String[] split = ip.split("\\.");
|
||||||
|
if (split.length != 4) return false;
|
||||||
|
for (int i = 0; i < 4; ++i) {
|
||||||
|
int p = Integer.parseInt(split[i]);
|
||||||
|
if (p > 255 || p < 0) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} catch (Exception ignore) {}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
478
old code/tray/src/qz/installer/certificate/CertificateManager.java
Executable file
478
old code/tray/src/qz/installer/certificate/CertificateManager.java
Executable file
@@ -0,0 +1,478 @@
|
|||||||
|
/**
|
||||||
|
* @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 qz.installer.certificate;
|
||||||
|
|
||||||
|
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
|
||||||
|
import org.bouncycastle.asn1.x500.AttributeTypeAndValue;
|
||||||
|
import org.bouncycastle.asn1.x500.RDN;
|
||||||
|
import org.bouncycastle.asn1.x500.X500Name;
|
||||||
|
import org.bouncycastle.asn1.x500.style.BCStyle;
|
||||||
|
import org.bouncycastle.cert.X509CertificateHolder;
|
||||||
|
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
|
||||||
|
import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder;
|
||||||
|
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||||
|
import org.bouncycastle.openssl.PEMKeyPair;
|
||||||
|
import org.bouncycastle.openssl.PEMParser;
|
||||||
|
import org.bouncycastle.openssl.jcajce.JcaMiscPEMGenerator;
|
||||||
|
import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
|
||||||
|
import org.bouncycastle.operator.OperatorException;
|
||||||
|
import org.eclipse.jetty.util.ssl.SslContextFactory;
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
import qz.common.Constants;
|
||||||
|
import qz.installer.Installer;
|
||||||
|
import qz.utils.ArgValue;
|
||||||
|
import qz.utils.FileUtilities;
|
||||||
|
import qz.utils.MacUtilities;
|
||||||
|
import qz.utils.SystemUtilities;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.nio.file.StandardOpenOption;
|
||||||
|
import java.security.*;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
import java.security.spec.PKCS8EncodedKeySpec;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
import static qz.utils.FileUtilities.*;
|
||||||
|
import static qz.installer.certificate.KeyPairWrapper.Type.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores and maintains reading and writing of certificate related files
|
||||||
|
*/
|
||||||
|
public class CertificateManager {
|
||||||
|
static List<Path> SAVE_LOCATIONS = new ArrayList<>();
|
||||||
|
static {
|
||||||
|
// Workaround for JDK-8266929
|
||||||
|
// See also https://github.com/qzind/tray/issues/814
|
||||||
|
SystemUtilities.clearAlgorithms();
|
||||||
|
|
||||||
|
// Skip shared location if running from IDE or build directory
|
||||||
|
// Prevents corrupting the version installed per https://github.com/qzind/tray/issues/1200
|
||||||
|
if(SystemUtilities.isJar() && SystemUtilities.isInstalled()) {
|
||||||
|
// Skip install location if running from sandbox (must remain sealed)
|
||||||
|
if(!SystemUtilities.isMac() || !MacUtilities.isSandboxed()) {
|
||||||
|
SAVE_LOCATIONS.add(SystemUtilities.getJarParentPath());
|
||||||
|
}
|
||||||
|
SAVE_LOCATIONS.add(SHARED_DIR);
|
||||||
|
}
|
||||||
|
SAVE_LOCATIONS.add(USER_DIR);
|
||||||
|
}
|
||||||
|
private static final Logger log = LogManager.getLogger(CertificateManager.class);
|
||||||
|
|
||||||
|
public static String DEFAULT_KEYSTORE_FORMAT = "PKCS12";
|
||||||
|
public static String DEFAULT_KEYSTORE_EXTENSION = ".p12";
|
||||||
|
public static String DEFAULT_CERTIFICATE_EXTENSION = ".crt";
|
||||||
|
private static int DEFAULT_PASSWORD_BITS = 100;
|
||||||
|
|
||||||
|
private boolean needsInstall;
|
||||||
|
private SslContextFactory.Server sslContextFactory;
|
||||||
|
private KeyPairWrapper sslKeyPair;
|
||||||
|
private KeyPairWrapper caKeyPair;
|
||||||
|
|
||||||
|
private Properties properties;
|
||||||
|
private char[] password;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For internal certs
|
||||||
|
*/
|
||||||
|
public CertificateManager(boolean forceNew, String ... hostNames) throws IOException, GeneralSecurityException, OperatorException {
|
||||||
|
Security.addProvider(new BouncyCastleProvider());
|
||||||
|
sslKeyPair = new KeyPairWrapper(SSL);
|
||||||
|
caKeyPair = new KeyPairWrapper(CA);
|
||||||
|
|
||||||
|
if (!forceNew) {
|
||||||
|
// order is important: ssl, ca
|
||||||
|
properties = loadProperties(sslKeyPair, caKeyPair);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(properties == null) {
|
||||||
|
log.warn("Warning, SSL properties won't be loaded from disk... we'll try to create them...");
|
||||||
|
|
||||||
|
CertificateChainBuilder cb = new CertificateChainBuilder(hostNames);
|
||||||
|
caKeyPair = cb.createCaCert();
|
||||||
|
sslKeyPair = cb.createSslCert(caKeyPair);
|
||||||
|
|
||||||
|
// Create CA
|
||||||
|
properties = createKeyStore(CA)
|
||||||
|
.writeCert(CA)
|
||||||
|
.writeKeystore(null, CA);
|
||||||
|
|
||||||
|
// Create SSL
|
||||||
|
properties = createKeyStore(SSL)
|
||||||
|
.writeCert(SSL)
|
||||||
|
.writeKeystore(properties, SSL);
|
||||||
|
|
||||||
|
// Save properties
|
||||||
|
saveProperties();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For trusted PEM-formatted certs
|
||||||
|
*/
|
||||||
|
public CertificateManager(File trustedPemKey, File trustedPemCert) throws Exception {
|
||||||
|
Security.addProvider(new BouncyCastleProvider());
|
||||||
|
needsInstall = false;
|
||||||
|
sslKeyPair = new KeyPairWrapper(SSL);
|
||||||
|
|
||||||
|
// Assumes ssl/privkey.pem, ssl/fullchain.pem
|
||||||
|
properties = createTrustedKeystore(trustedPemKey, trustedPemCert)
|
||||||
|
.writeKeystore(properties, SSL);
|
||||||
|
|
||||||
|
// Save properties
|
||||||
|
saveProperties();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For trusted PKCS12-formatted certs
|
||||||
|
*/
|
||||||
|
public CertificateManager(File pkcs12File, char[] password) throws Exception {
|
||||||
|
Security.addProvider(new BouncyCastleProvider());
|
||||||
|
needsInstall = false;
|
||||||
|
sslKeyPair = new KeyPairWrapper(SSL);
|
||||||
|
|
||||||
|
// Assumes direct pkcs12 import
|
||||||
|
this.password = password;
|
||||||
|
sslKeyPair.init(pkcs12File, password);
|
||||||
|
|
||||||
|
// Save it back, but to a location we can find
|
||||||
|
properties = writeKeystore(null, SSL);
|
||||||
|
|
||||||
|
// Save properties
|
||||||
|
saveProperties();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void renewCertChain(String ... hostNames) throws Exception {
|
||||||
|
CertificateChainBuilder cb = new CertificateChainBuilder(hostNames);
|
||||||
|
sslKeyPair = cb.createSslCert(caKeyPair);
|
||||||
|
createKeyStore(SSL).writeKeystore(properties, SSL);
|
||||||
|
reloadSslContextFactory();
|
||||||
|
}
|
||||||
|
|
||||||
|
public KeyPairWrapper getSslKeyPair() {
|
||||||
|
return sslKeyPair;
|
||||||
|
}
|
||||||
|
|
||||||
|
public KeyPairWrapper getCaKeyPair() {
|
||||||
|
return caKeyPair;
|
||||||
|
}
|
||||||
|
|
||||||
|
public KeyPairWrapper getKeyPair(KeyPairWrapper.Type type) {
|
||||||
|
switch(type) {
|
||||||
|
case SSL:
|
||||||
|
return sslKeyPair;
|
||||||
|
case CA:
|
||||||
|
default:
|
||||||
|
return caKeyPair;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public KeyPairWrapper getKeyPair(String alias) {
|
||||||
|
for(KeyPairWrapper.Type type : KeyPairWrapper.Type.values()) {
|
||||||
|
if (KeyPairWrapper.getAlias(type).equalsIgnoreCase(alias)) {
|
||||||
|
return getKeyPair(type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return getKeyPair(KeyPairWrapper.Type.CA);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Properties getProperties() {
|
||||||
|
return properties;
|
||||||
|
}
|
||||||
|
|
||||||
|
private char[] getPassword() {
|
||||||
|
if (password == null) {
|
||||||
|
if(caKeyPair != null && caKeyPair.getPassword() != null) {
|
||||||
|
// Reuse existing
|
||||||
|
password = caKeyPair.getPassword();
|
||||||
|
} else {
|
||||||
|
// Create new
|
||||||
|
BigInteger bi = new BigInteger(DEFAULT_PASSWORD_BITS, new SecureRandom());
|
||||||
|
password = bi.toString(16).toCharArray();
|
||||||
|
log.info("Created a random {} bit password: {}", DEFAULT_PASSWORD_BITS, new String(password));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return password;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SslContextFactory.Server configureSslContextFactory() {
|
||||||
|
sslContextFactory = new SslContextFactory.Server();
|
||||||
|
sslContextFactory.setKeyStore(sslKeyPair.getKeyStore());
|
||||||
|
sslContextFactory.setKeyStorePassword(sslKeyPair.getPasswordString());
|
||||||
|
sslContextFactory.setKeyManagerPassword(sslKeyPair.getPasswordString());
|
||||||
|
return sslContextFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void reloadSslContextFactory() throws Exception {
|
||||||
|
if(isSslActive()) {
|
||||||
|
sslContextFactory.reload(sslContextFactory -> {
|
||||||
|
sslContextFactory.setKeyStore(sslKeyPair.getKeyStore());
|
||||||
|
sslContextFactory.setKeyStorePassword(sslKeyPair.getPasswordString());
|
||||||
|
sslContextFactory.setKeyManagerPassword(sslKeyPair.getPasswordString());
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
log.warn("SSL isn't active, can't reload");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isSslActive() {
|
||||||
|
return sslContextFactory != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean needsInstall() {
|
||||||
|
return needsInstall;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CertificateManager createKeyStore(KeyPairWrapper.Type type) throws IOException, GeneralSecurityException {
|
||||||
|
KeyPairWrapper keyPair = type == CA ? caKeyPair : sslKeyPair;
|
||||||
|
KeyStore keyStore = KeyStore.getInstance(DEFAULT_KEYSTORE_FORMAT);
|
||||||
|
keyStore.load(null, password);
|
||||||
|
|
||||||
|
List<X509Certificate> chain = new ArrayList<>();
|
||||||
|
chain.add(keyPair.getCert());
|
||||||
|
|
||||||
|
// Add ca to ssl cert chain
|
||||||
|
if (keyPair.getType() == SSL) {
|
||||||
|
chain.add(caKeyPair.getCert());
|
||||||
|
}
|
||||||
|
keyStore.setEntry(caKeyPair.getAlias(), new KeyStore.TrustedCertificateEntry(caKeyPair.getCert()), null);
|
||||||
|
keyStore.setKeyEntry(keyPair.getAlias(), keyPair.getKey(), getPassword(), chain.toArray(new X509Certificate[chain.size()]));
|
||||||
|
keyPair.init(keyStore, getPassword());
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CertificateManager createTrustedKeystore(File p12Store, String password) throws Exception {
|
||||||
|
sslKeyPair = new KeyPairWrapper(SSL);
|
||||||
|
sslKeyPair.init(p12Store, password.toCharArray());
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CertificateManager createTrustedKeystore(File pemKey, File pemCert) throws Exception {
|
||||||
|
sslKeyPair = new KeyPairWrapper(SSL);
|
||||||
|
|
||||||
|
// Private Key
|
||||||
|
PEMParser pem = new PEMParser(new FileReader(pemKey));
|
||||||
|
Object parsedObject = pem.readObject();
|
||||||
|
|
||||||
|
PrivateKeyInfo privateKeyInfo = parsedObject instanceof PEMKeyPair ? ((PEMKeyPair)parsedObject).getPrivateKeyInfo() : (PrivateKeyInfo)parsedObject;
|
||||||
|
PKCS8EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(privateKeyInfo.getEncoded());
|
||||||
|
KeyFactory factory = KeyFactory.getInstance("RSA");
|
||||||
|
PrivateKey key = factory.generatePrivate(privateKeySpec);
|
||||||
|
|
||||||
|
List<X509Certificate> certs = new ArrayList<>();
|
||||||
|
X509CertificateHolder certHolder = (X509CertificateHolder)pem.readObject();
|
||||||
|
if(certHolder != null) {
|
||||||
|
certs.add(new JcaX509CertificateConverter().setProvider("BC").getCertificate(certHolder));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Certificate
|
||||||
|
pem = new PEMParser(new FileReader(pemCert));
|
||||||
|
while((certHolder = (X509CertificateHolder)pem.readObject()) != null) {
|
||||||
|
certs.add(new JcaX509CertificateConverter().setProvider("BC").getCertificate(certHolder));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keystore
|
||||||
|
KeyStore ks = KeyStore.getInstance("PKCS12");
|
||||||
|
ks.load(null);
|
||||||
|
|
||||||
|
for (int i = 0; i < certs.size(); i++) {
|
||||||
|
ks.setCertificateEntry(sslKeyPair.getAlias() + "_" + i, certs.get(i));
|
||||||
|
}
|
||||||
|
|
||||||
|
KeyStore keyStore = KeyStore.getInstance("PKCS12");
|
||||||
|
keyStore.load(null);
|
||||||
|
keyStore.setKeyEntry(sslKeyPair.getAlias(), key, getPassword(), certs.toArray(new X509Certificate[certs.size()]));
|
||||||
|
|
||||||
|
sslKeyPair.init(keyStore, getPassword());
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void writeCert(X509Certificate data, File dest) throws IOException {
|
||||||
|
// PEMWriter doesn't always clear the file, explicitly delete it, see issue #796
|
||||||
|
if(dest.exists()) {
|
||||||
|
dest.delete();
|
||||||
|
}
|
||||||
|
JcaMiscPEMGenerator cert = new JcaMiscPEMGenerator(data);
|
||||||
|
JcaPEMWriter writer = new JcaPEMWriter(new OutputStreamWriter(Files.newOutputStream(dest.toPath(), StandardOpenOption.CREATE)));
|
||||||
|
writer.writeObject(cert.generate());
|
||||||
|
writer.close();
|
||||||
|
FileUtilities.inheritParentPermissions(dest.toPath());
|
||||||
|
log.info("Wrote Cert: \"{}\"", dest);
|
||||||
|
}
|
||||||
|
|
||||||
|
public CertificateManager writeCert(KeyPairWrapper.Type type) throws IOException {
|
||||||
|
KeyPairWrapper keyPair = type == CA ? caKeyPair : sslKeyPair;
|
||||||
|
File certFile = new File(getWritableLocation("ssl"), keyPair.getAlias() + DEFAULT_CERTIFICATE_EXTENSION);
|
||||||
|
|
||||||
|
writeCert(keyPair.getCert(), certFile);
|
||||||
|
FileUtilities.inheritParentPermissions(certFile.toPath());
|
||||||
|
if(keyPair.getType() == CA) {
|
||||||
|
needsInstall = true;
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Properties writeKeystore(Properties props, KeyPairWrapper.Type type) throws GeneralSecurityException, IOException {
|
||||||
|
File sslDir = getWritableLocation("ssl");
|
||||||
|
KeyPairWrapper keyPair = type == CA ? caKeyPair : sslKeyPair;
|
||||||
|
|
||||||
|
File keyFile = new File(sslDir, keyPair.getAlias() + DEFAULT_KEYSTORE_EXTENSION);
|
||||||
|
keyPair.getKeyStore().store(Files.newOutputStream(keyFile.toPath(), StandardOpenOption.CREATE), getPassword());
|
||||||
|
FileUtilities.inheritParentPermissions(keyFile.toPath());
|
||||||
|
log.info("Wrote {} Key: \"{}\"", DEFAULT_KEYSTORE_FORMAT, keyFile);
|
||||||
|
|
||||||
|
if (props == null) {
|
||||||
|
props = new Properties();
|
||||||
|
}
|
||||||
|
props.putIfAbsent(String.format("%s.keystore", keyPair.propsPrefix()), keyFile.toString());
|
||||||
|
props.putIfAbsent(String.format("%s.storepass", keyPair.propsPrefix()), new String(getPassword()));
|
||||||
|
props.putIfAbsent(String.format("%s.alias", keyPair.propsPrefix()), keyPair.getAlias());
|
||||||
|
|
||||||
|
if (keyPair.getType() == SSL) {
|
||||||
|
props.putIfAbsent(String.format("%s.host", keyPair.propsPrefix()), ArgValue.SECURITY_WSS_HOST.getDefaultVal());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return props;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static File getWritableLocation(String ... suffixes) throws IOException {
|
||||||
|
// Get an array of preferred directories
|
||||||
|
ArrayList<Path> locs = new ArrayList<>();
|
||||||
|
|
||||||
|
if (suffixes.length == 0) {
|
||||||
|
locs.addAll(SAVE_LOCATIONS);
|
||||||
|
// Last, fallback on a directory we won't ever see again :/
|
||||||
|
locs.add(TEMP_DIR);
|
||||||
|
} else {
|
||||||
|
// Same as above, but with suffixes added (usually "ssl"), skipping the install location
|
||||||
|
for(Path saveLocation : SAVE_LOCATIONS) {
|
||||||
|
if(!saveLocation.equals(SystemUtilities.getJarParentPath())) {
|
||||||
|
locs.add(Paths.get(saveLocation.toString(), suffixes));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Last, fallback on a directory we won't ever see again :/
|
||||||
|
locs.add(Paths.get(TEMP_DIR.toString(), suffixes));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find a suitable write location
|
||||||
|
File path;
|
||||||
|
for(Path loc : locs) {
|
||||||
|
if (loc == null) continue;
|
||||||
|
boolean isPreferred = locs.indexOf(loc) == 0;
|
||||||
|
path = loc.toFile();
|
||||||
|
path.mkdirs();
|
||||||
|
if (path.canWrite()) {
|
||||||
|
log.debug("Writing to {}", loc);
|
||||||
|
if(!isPreferred) {
|
||||||
|
log.warn("Warning, {} isn't the preferred write location, but we'll use it anyway", loc);
|
||||||
|
}
|
||||||
|
return path;
|
||||||
|
} else {
|
||||||
|
log.debug("Can't write to {}, trying the next...", loc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new IOException("Can't find a suitable write location. SSL will fail.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Properties loadProperties(KeyPairWrapper... keyPairs) {
|
||||||
|
log.info("Try to find SSL properties file...");
|
||||||
|
|
||||||
|
|
||||||
|
Properties props = null;
|
||||||
|
for(Path loc : SAVE_LOCATIONS) {
|
||||||
|
if (loc == null) continue;
|
||||||
|
try {
|
||||||
|
for(KeyPairWrapper keyPair : keyPairs) {
|
||||||
|
props = loadKeyPair(keyPair, loc, props);
|
||||||
|
}
|
||||||
|
// We've loaded without Exception, return
|
||||||
|
log.info("Found {}/{}.properties", loc, Constants.PROPS_FILE);
|
||||||
|
return props;
|
||||||
|
} catch(Exception ignore) {
|
||||||
|
log.warn("Properties couldn't be loaded at {}, trying fallback...", loc, ignore);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.info("Could not get SSL properties from file.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Properties loadKeyPair(KeyPairWrapper keyPair, Path parent, Properties existing) throws Exception {
|
||||||
|
Properties props;
|
||||||
|
|
||||||
|
if (existing == null) {
|
||||||
|
FileInputStream fis = null;
|
||||||
|
try {
|
||||||
|
props = new Properties();
|
||||||
|
props.load(fis = new FileInputStream(new File(parent.toFile(), Constants.PROPS_FILE + ".properties")));
|
||||||
|
} finally {
|
||||||
|
if(fis != null) fis.close();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
props = existing;
|
||||||
|
}
|
||||||
|
|
||||||
|
String ks = props.getProperty(String.format("%s.keystore", keyPair.propsPrefix()));
|
||||||
|
String pw = props.getProperty(String.format("%s.storepass", keyPair.propsPrefix()), "");
|
||||||
|
|
||||||
|
if(ks == null || ks.trim().isEmpty()) {
|
||||||
|
if(keyPair.getType() == SSL) {
|
||||||
|
throw new IOException("Missing wss.keystore entry");
|
||||||
|
} else {
|
||||||
|
// CA is only needed for internal certs, return
|
||||||
|
return props;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
File ksFile = Paths.get(ks).isAbsolute()? new File(ks):new File(parent.toFile(), ks);
|
||||||
|
if (ksFile.exists()) {
|
||||||
|
keyPair.init(ksFile, pw.toCharArray());
|
||||||
|
return props;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveProperties() throws IOException {
|
||||||
|
File propsFile = new File(getWritableLocation(), Constants.PROPS_FILE + ".properties");
|
||||||
|
Installer.persistProperties(propsFile, properties); // checks for props from previous install
|
||||||
|
properties.store(new FileOutputStream(propsFile), null);
|
||||||
|
FileUtilities.inheritParentPermissions(propsFile.toPath());
|
||||||
|
log.info("Successfully created SSL properties file: {}", propsFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean emailMatches(X509Certificate cert) {
|
||||||
|
return emailMatches(cert, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean emailMatches(X509Certificate cert, boolean quiet) {
|
||||||
|
try {
|
||||||
|
X500Name x500name = new JcaX509CertificateHolder(cert).getSubject();
|
||||||
|
RDN[] emailNames = x500name.getRDNs(BCStyle.E);
|
||||||
|
for(RDN emailName : emailNames) {
|
||||||
|
AttributeTypeAndValue first = emailName.getFirst();
|
||||||
|
if (first != null && first.getValue() != null && Constants.ABOUT_EMAIL.equals(first.getValue().toString())) {
|
||||||
|
if(!quiet) {
|
||||||
|
log.info("Email address {} found, assuming CertProvider is {}", Constants.ABOUT_EMAIL, ExpiryTask.CertProvider.INTERNAL);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(Exception ignore) {}
|
||||||
|
if(!quiet) {
|
||||||
|
log.info("Email address {} was not found. Assuming the certificate is manually installed, we won't try to renew it.", Constants.ABOUT_EMAIL);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
295
old code/tray/src/qz/installer/certificate/ExpiryTask.java
Executable file
295
old code/tray/src/qz/installer/certificate/ExpiryTask.java
Executable file
@@ -0,0 +1,295 @@
|
|||||||
|
/**
|
||||||
|
* @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 qz.installer.certificate;
|
||||||
|
|
||||||
|
import org.bouncycastle.asn1.x500.X500Name;
|
||||||
|
import org.bouncycastle.asn1.x500.style.BCStyle;
|
||||||
|
import org.bouncycastle.asn1.x509.GeneralName;
|
||||||
|
import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder;
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
import qz.common.Constants;
|
||||||
|
import qz.utils.ShellUtilities;
|
||||||
|
import qz.utils.SystemUtilities;
|
||||||
|
|
||||||
|
import javax.naming.InvalidNameException;
|
||||||
|
import javax.naming.ldap.LdapName;
|
||||||
|
import javax.naming.ldap.Rdn;
|
||||||
|
import java.io.File;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.security.cert.CertificateException;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
import static qz.utils.FileUtilities.*;
|
||||||
|
|
||||||
|
public class ExpiryTask extends TimerTask {
|
||||||
|
private static final Logger log = LogManager.getLogger(CertificateManager.class);
|
||||||
|
public static final int DEFAULT_INITIAL_DELAY = 60 * 1000; // 1 minute
|
||||||
|
public static final int DEFAULT_CHECK_FREQUENCY = 3600 * 1000; // 1 hour
|
||||||
|
private static final int DEFAULT_GRACE_PERIOD_DAYS = 5;
|
||||||
|
private enum ExpiryState {VALID, EXPIRING, EXPIRED, MANAGED}
|
||||||
|
|
||||||
|
public enum CertProvider {
|
||||||
|
INTERNAL(Constants.ABOUT_COMPANY + ".*"),
|
||||||
|
LETS_ENCRYPT("Let's Encrypt.*"),
|
||||||
|
CA_CERT_ORG("CA Cert Signing.*"),
|
||||||
|
UNKNOWN;
|
||||||
|
String[] patterns;
|
||||||
|
CertProvider(String ... regexPattern) {
|
||||||
|
this.patterns = regexPattern;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Timer timer;
|
||||||
|
private CertificateManager certificateManager;
|
||||||
|
private String[] hostNames;
|
||||||
|
private CertProvider certProvider;
|
||||||
|
|
||||||
|
public ExpiryTask(CertificateManager certificateManager) {
|
||||||
|
super();
|
||||||
|
this.certificateManager = certificateManager;
|
||||||
|
this.hostNames = parseHostNames();
|
||||||
|
this.certProvider = findCertProvider();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
// Check for expiration
|
||||||
|
ExpiryState state = getExpiry(certificateManager.getSslKeyPair().getCert());
|
||||||
|
switch(state) {
|
||||||
|
case EXPIRING:
|
||||||
|
case EXPIRED:
|
||||||
|
log.info("Certificate ExpiryState {}, renewing/reloading...", state);
|
||||||
|
switch(certProvider) {
|
||||||
|
case INTERNAL:
|
||||||
|
if(renewInternalCert()) {
|
||||||
|
getExpiry();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case CA_CERT_ORG:
|
||||||
|
case LETS_ENCRYPT:
|
||||||
|
if(renewExternalCert(certProvider)) {
|
||||||
|
getExpiry();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case UNKNOWN:
|
||||||
|
default:
|
||||||
|
log.warn("Certificate can't be renewed/reloaded; ExpiryState: {}, CertProvider: {}", state, certProvider);
|
||||||
|
}
|
||||||
|
case VALID:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean renewInternalCert() {
|
||||||
|
try {
|
||||||
|
log.info("Requesting a new SSL certificate from {} ...", certificateManager.getCaKeyPair().getAlias());
|
||||||
|
certificateManager.renewCertChain(hostNames);
|
||||||
|
log.info("New SSL certificate created. Reloading SslContextFactory...");
|
||||||
|
certificateManager.reloadSslContextFactory();
|
||||||
|
log.info("Reloaded SSL successfully.");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch(Exception e) {
|
||||||
|
log.error("Could not reload SSL certificate", e);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ExpiryState getExpiry() {
|
||||||
|
return getExpiry(certificateManager.getSslKeyPair().getCert());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the SSL certificate is generated by QZ Tray and expires inside the GRACE_PERIOD.
|
||||||
|
* GRACE_PERIOD is preferred for scheduling the renewals in advance, such as non-peak hours
|
||||||
|
*/
|
||||||
|
public static ExpiryState getExpiry(X509Certificate cert) {
|
||||||
|
// Invalid
|
||||||
|
if (cert == null) {
|
||||||
|
log.error("Can't check for expiration, certificate is missing.");
|
||||||
|
return ExpiryState.EXPIRED;
|
||||||
|
}
|
||||||
|
|
||||||
|
Date expireDate = cert.getNotAfter();
|
||||||
|
Calendar now = Calendar.getInstance(Locale.ENGLISH);
|
||||||
|
Calendar expires = Calendar.getInstance(Locale.ENGLISH);
|
||||||
|
expires.setTime(expireDate);
|
||||||
|
|
||||||
|
// Expired
|
||||||
|
if (now.after(expires)) {
|
||||||
|
log.info("SSL certificate has expired {}. It must be renewed immediately.", SystemUtilities.toISO(expireDate));
|
||||||
|
return ExpiryState.EXPIRED;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expiring
|
||||||
|
expires.add(Calendar.DAY_OF_YEAR, -DEFAULT_GRACE_PERIOD_DAYS);
|
||||||
|
if (now.after(expires)) {
|
||||||
|
log.info("SSL certificate will expire in less than {} days: {}", DEFAULT_GRACE_PERIOD_DAYS, SystemUtilities.toISO(expireDate));
|
||||||
|
return ExpiryState.EXPIRING;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Valid
|
||||||
|
int days = (int)Math.round((expireDate.getTime() - new Date().getTime()) / (double)86400000);
|
||||||
|
log.info("SSL certificate is still valid for {} more days: {}. We'll make a new one automatically when needed.", days, SystemUtilities.toISO(expireDate));
|
||||||
|
return ExpiryState.VALID;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void schedule() {
|
||||||
|
schedule(DEFAULT_INITIAL_DELAY, DEFAULT_CHECK_FREQUENCY);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void schedule(int delayMillis, int freqMillis) {
|
||||||
|
if(timer != null) {
|
||||||
|
timer.cancel();
|
||||||
|
timer.purge();
|
||||||
|
}
|
||||||
|
timer = new Timer();
|
||||||
|
timer.scheduleAtFixedRate(this, delayMillis, freqMillis);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String[] parseHostNames() {
|
||||||
|
return parseHostNames(certificateManager.getSslKeyPair().getCert());
|
||||||
|
}
|
||||||
|
|
||||||
|
public CertProvider findCertProvider() {
|
||||||
|
return findCertProvider(certificateManager.getSslKeyPair().getCert());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CertProvider findCertProvider(X509Certificate cert) {
|
||||||
|
// Internal certs use CN=localhost, trust email instead
|
||||||
|
if (CertificateManager.emailMatches(cert)) {
|
||||||
|
return CertProvider.INTERNAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
String providerDN;
|
||||||
|
|
||||||
|
// check registered patterns to classify certificate
|
||||||
|
if(cert.getIssuerDN() != null && (providerDN = cert.getIssuerDN().getName()) != null) {
|
||||||
|
String cn = null;
|
||||||
|
try {
|
||||||
|
// parse issuer's DN
|
||||||
|
LdapName ldapName = new LdapName(providerDN);
|
||||||
|
for(Rdn rdn : ldapName.getRdns()) {
|
||||||
|
if(rdn.getType().equalsIgnoreCase("CN")) {
|
||||||
|
cn = (String)rdn.getValue();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// compare cn to our pattern
|
||||||
|
if(cn != null) {
|
||||||
|
for(CertProvider provider : CertProvider.values()) {
|
||||||
|
for(String pattern : provider.patterns) {
|
||||||
|
if (cn.matches(pattern)) {
|
||||||
|
log.warn("Cert issuer detected as {}", provider.name());
|
||||||
|
return provider;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch(InvalidNameException ignore) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.warn("A valid issuer couldn't be found, we won't know how to renew this cert when it expires");
|
||||||
|
return CertProvider.UNKNOWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String[] parseHostNames(X509Certificate cert) {
|
||||||
|
// Cache the SAN hosts for recreation
|
||||||
|
List<String> hostNameList = new ArrayList<>();
|
||||||
|
try {
|
||||||
|
Collection<List<?>> altNames = cert.getSubjectAlternativeNames();
|
||||||
|
if (altNames != null) {
|
||||||
|
for(List<?> altName : altNames) {
|
||||||
|
if(altName.size()< 1) continue;
|
||||||
|
switch((Integer)altName.get(0)) {
|
||||||
|
case GeneralName.dNSName:
|
||||||
|
case GeneralName.iPAddress:
|
||||||
|
Object data = altName.get(1);
|
||||||
|
if (data instanceof String) {
|
||||||
|
hostNameList.add(((String)data));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.error("getSubjectAlternativeNames is null?");
|
||||||
|
}
|
||||||
|
log.debug("Parsed hostNames: {}", String.join(", ", hostNameList));
|
||||||
|
} catch(CertificateException e) {
|
||||||
|
log.warn("Can't parse hostNames from this cert. Cert renewals will contain default values instead");
|
||||||
|
}
|
||||||
|
return hostNameList.toArray(new String[hostNameList.size()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean renewExternalCert(CertProvider externalProvider) {
|
||||||
|
switch(externalProvider) {
|
||||||
|
case LETS_ENCRYPT:
|
||||||
|
return renewLetsEncryptCert(externalProvider);
|
||||||
|
case CA_CERT_ORG:
|
||||||
|
default:
|
||||||
|
log.error("Cert renewal for {} is not implemented", externalProvider);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean renewLetsEncryptCert(CertProvider externalProvider) {
|
||||||
|
try {
|
||||||
|
File storagePath = CertificateManager.getWritableLocation("ssl");
|
||||||
|
|
||||||
|
// cerbot is much simpler than acme, let's use it
|
||||||
|
Path root = Paths.get(SHARED_DIR.toString(), "letsencrypt", "config");
|
||||||
|
log.info("Attempting to renew {}. Assuming certs are installed in {}...", externalProvider, root);
|
||||||
|
List<String> cmds = new ArrayList(Arrays.asList("certbot", "--force-renewal", "certonly"));
|
||||||
|
|
||||||
|
cmds.add("--standalone");
|
||||||
|
|
||||||
|
cmds.add("--config-dir");
|
||||||
|
String config = Paths.get(SHARED_DIR.toString(), "ssl", "letsencrypt", "config").toString();
|
||||||
|
cmds.add(config);
|
||||||
|
|
||||||
|
cmds.add("--logs-dir");
|
||||||
|
cmds.add(Paths.get(SHARED_DIR.toString(), "ssl", "letsencrypt", "logs").toString());
|
||||||
|
|
||||||
|
cmds.add("--work-dir");
|
||||||
|
cmds.add(Paths.get(SHARED_DIR.toString(), "ssl", "letsencrypt").toString());
|
||||||
|
|
||||||
|
// append dns names
|
||||||
|
for(String hostName : hostNames) {
|
||||||
|
cmds.add("-d");
|
||||||
|
cmds.add(hostName);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ShellUtilities.execute(cmds.toArray(new String[cmds.size()]))) {
|
||||||
|
// Assume the cert is stored in a folder called "letsencrypt/config/live/<domain>"
|
||||||
|
Path keyPath = Paths.get(config, "live", hostNames[0], "privkey.pem");
|
||||||
|
Path certPath = Paths.get(config, "live", hostNames[0], "fullchain.pem"); // fullchain required
|
||||||
|
certificateManager.createTrustedKeystore(keyPath.toFile(), certPath.toFile());
|
||||||
|
log.info("Files imported, converted and saved. Reloading SslContextFactory...");
|
||||||
|
certificateManager.reloadSslContextFactory();
|
||||||
|
log.info("Reloaded SSL successfully.");
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
log.warn("Something went wrong renewing the LetsEncrypt certificate. Please run the certbot command manually to learn more.");
|
||||||
|
}
|
||||||
|
} catch(Exception e) {
|
||||||
|
log.error("Error renewing/reloading LetsEncrypt cert", e);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
130
old code/tray/src/qz/installer/certificate/KeyPairWrapper.java
Executable file
130
old code/tray/src/qz/installer/certificate/KeyPairWrapper.java
Executable file
@@ -0,0 +1,130 @@
|
|||||||
|
/**
|
||||||
|
* @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 qz.installer.certificate;
|
||||||
|
|
||||||
|
import qz.common.Constants;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.security.GeneralSecurityException;
|
||||||
|
import java.security.KeyPair;
|
||||||
|
import java.security.KeyStore;
|
||||||
|
import java.security.PrivateKey;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
import java.util.Enumeration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrap handling of X509Certificate, PrivateKey and KeyStore conversion
|
||||||
|
*/
|
||||||
|
public class KeyPairWrapper {
|
||||||
|
public enum Type {CA, SSL}
|
||||||
|
|
||||||
|
private Type type;
|
||||||
|
private PrivateKey key;
|
||||||
|
private char[] password;
|
||||||
|
private X509Certificate cert;
|
||||||
|
private KeyStore keyStore; // for SSL
|
||||||
|
|
||||||
|
public KeyPairWrapper(Type type) {
|
||||||
|
this.type = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public KeyPairWrapper(Type type, KeyPair keyPair, X509Certificate cert) {
|
||||||
|
this.type = type;
|
||||||
|
this.key = keyPair.getPrivate();
|
||||||
|
this.cert = cert;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load from disk
|
||||||
|
*/
|
||||||
|
public void init(File keyFile, char[] password) throws IOException, GeneralSecurityException {
|
||||||
|
KeyStore keyStore = KeyStore.getInstance(keyFile.getName().endsWith(".jks") ? "JKS" : "PKCS12");
|
||||||
|
keyStore.load(new FileInputStream(keyFile), password);
|
||||||
|
init(keyStore, password);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load from memory
|
||||||
|
*/
|
||||||
|
public void init(KeyStore keyStore, char[] password) throws GeneralSecurityException {
|
||||||
|
this.keyStore = keyStore;
|
||||||
|
KeyStore.ProtectionParameter param = new KeyStore.PasswordProtection(password);
|
||||||
|
KeyStore.PrivateKeyEntry entry = (KeyStore.PrivateKeyEntry)keyStore.getEntry(getAlias(), param);
|
||||||
|
// the entry we assume is always wrong for pkcs12 imports, search for it instead
|
||||||
|
if(entry == null) {
|
||||||
|
Enumeration<String> enumerator = keyStore.aliases();
|
||||||
|
while(enumerator.hasMoreElements()) {
|
||||||
|
String alias = enumerator.nextElement();
|
||||||
|
if(keyStore.isKeyEntry(alias)) {
|
||||||
|
this.password = password;
|
||||||
|
this.key = ((KeyStore.PrivateKeyEntry)keyStore.getEntry(alias, param)).getPrivateKey();
|
||||||
|
this.cert = (X509Certificate)keyStore.getCertificate(alias);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new GeneralSecurityException("Could not initialize the KeyStore for internal use");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.password = password;
|
||||||
|
this.key = entry.getPrivateKey();
|
||||||
|
this.cert = (X509Certificate)keyStore.getCertificate(getAlias());
|
||||||
|
}
|
||||||
|
|
||||||
|
public X509Certificate getCert() {
|
||||||
|
return cert;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PrivateKey getKey() {
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPasswordString() {
|
||||||
|
return new String(password);
|
||||||
|
}
|
||||||
|
|
||||||
|
public char[] getPassword() {
|
||||||
|
return password;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getAlias(Type type) {
|
||||||
|
switch(type) {
|
||||||
|
case SSL:
|
||||||
|
return Constants.PROPS_FILE; // "qz-tray"
|
||||||
|
case CA:
|
||||||
|
default:
|
||||||
|
return "root-ca";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAlias() {
|
||||||
|
return getAlias(getType());
|
||||||
|
}
|
||||||
|
|
||||||
|
public String propsPrefix() {
|
||||||
|
switch(type) {
|
||||||
|
case SSL:
|
||||||
|
return "wss";
|
||||||
|
case CA:
|
||||||
|
default:
|
||||||
|
return "ca";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Type getType() {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public KeyStore getKeyStore() {
|
||||||
|
return keyStore;
|
||||||
|
}
|
||||||
|
}
|
||||||
365
old code/tray/src/qz/installer/certificate/LinuxCertificateInstaller.java
Executable file
365
old code/tray/src/qz/installer/certificate/LinuxCertificateInstaller.java
Executable file
@@ -0,0 +1,365 @@
|
|||||||
|
/**
|
||||||
|
* @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 qz.installer.certificate;
|
||||||
|
|
||||||
|
import org.apache.commons.io.FileUtils;
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
import org.bouncycastle.asn1.DEROctetString;
|
||||||
|
import org.bouncycastle.asn1.x509.Extension;
|
||||||
|
import org.bouncycastle.asn1.x509.SubjectKeyIdentifier;
|
||||||
|
import org.bouncycastle.util.encoders.Base64;
|
||||||
|
import qz.auth.X509Constants;
|
||||||
|
import qz.common.Constants;
|
||||||
|
import qz.installer.Installer;
|
||||||
|
import qz.utils.ByteUtilities;
|
||||||
|
import qz.utils.ShellUtilities;
|
||||||
|
import qz.utils.SystemUtilities;
|
||||||
|
import qz.utils.UnixUtilities;
|
||||||
|
|
||||||
|
import javax.swing.*;
|
||||||
|
import java.awt.*;
|
||||||
|
import java.io.*;
|
||||||
|
import java.security.cert.CertificateException;
|
||||||
|
import java.security.cert.CertificateFactory;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static qz.installer.Installer.PrivilegeLevel.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Tres Finocchiaro
|
||||||
|
*/
|
||||||
|
public class LinuxCertificateInstaller extends NativeCertificateInstaller {
|
||||||
|
private static final Logger log = LogManager.getLogger(LinuxCertificateInstaller.class);
|
||||||
|
private static final String CA_CERTIFICATES = "/usr/local/share/ca-certificates/";
|
||||||
|
private static final String CA_CERTIFICATE_NAME = Constants.PROPS_FILE + "-root.crt"; // e.g. qz-tray-root.crt
|
||||||
|
private static final String PK11_KIT_ID = "pkcs11:id=";
|
||||||
|
|
||||||
|
private static String[] NSSDB_URLS = {
|
||||||
|
// Conventional cert store
|
||||||
|
"sql:" + System.getenv("HOME") + "/.pki/nssdb/",
|
||||||
|
|
||||||
|
// Snap-specific cert stores
|
||||||
|
"sql:" + System.getenv("HOME") + "/snap/chromium/current/.pki/nssdb/",
|
||||||
|
"sql:" + System.getenv("HOME") + "/snap/brave/current/.pki/nssdb/",
|
||||||
|
"sql:" + System.getenv("HOME") + "/snap/opera/current/.pki/nssdb/",
|
||||||
|
"sql:" + System.getenv("HOME") + "/snap/opera-beta/current/.pki/nssdb/"
|
||||||
|
};
|
||||||
|
|
||||||
|
private Installer.PrivilegeLevel certType;
|
||||||
|
|
||||||
|
public LinuxCertificateInstaller(Installer.PrivilegeLevel certType) {
|
||||||
|
setInstallType(certType);
|
||||||
|
findCertutil();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Installer.PrivilegeLevel getInstallType() {
|
||||||
|
return certType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setInstallType(Installer.PrivilegeLevel certType) {
|
||||||
|
this.certType = certType;
|
||||||
|
if (this.certType == SYSTEM) {
|
||||||
|
log.warn("Command \"certutil\" (required for certain browsers) needs to run as USER. We'll try again on launch.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean remove(List<String> idList) {
|
||||||
|
boolean success = true;
|
||||||
|
if(certType == SYSTEM) {
|
||||||
|
boolean first = distrustUsingUpdateCaCertificates(idList);
|
||||||
|
boolean second = distrustUsingTrustAnchor(idList);
|
||||||
|
success = first || second;
|
||||||
|
} else {
|
||||||
|
for(String nickname : idList) {
|
||||||
|
for(String nssdb : NSSDB_URLS) {
|
||||||
|
success = success && ShellUtilities.execute("certutil", "-d", nssdb, "-D", "-n", nickname);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> find() {
|
||||||
|
ArrayList<String> nicknames = new ArrayList<>();
|
||||||
|
if(certType == SYSTEM) {
|
||||||
|
nicknames = findUsingTrustAnchor();
|
||||||
|
nicknames.addAll(findUsingUsingUpdateCaCert());
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
for(String nssdb : NSSDB_URLS) {
|
||||||
|
Process p = Runtime.getRuntime().exec(new String[] {"certutil", "-d", nssdb, "-L"});
|
||||||
|
BufferedReader in = new BufferedReader(new InputStreamReader(p.getInputStream()));
|
||||||
|
String line;
|
||||||
|
while((line = in.readLine()) != null) {
|
||||||
|
if (line.startsWith(Constants.ABOUT_COMPANY + " ")) {
|
||||||
|
nicknames.add(Constants.ABOUT_COMPANY);
|
||||||
|
break; // Stop reading input; nicknames can't appear more than once
|
||||||
|
}
|
||||||
|
}
|
||||||
|
in.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(IOException e) {
|
||||||
|
log.warn("Could not get certificate nicknames", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nicknames;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean verify(File ignore) { return true; } // no easy way to validate a cert, assume it's installed
|
||||||
|
|
||||||
|
public boolean add(File certFile) {
|
||||||
|
boolean success = true;
|
||||||
|
|
||||||
|
if(certType == SYSTEM) {
|
||||||
|
// Attempt two common methods for installing the SSL certificate
|
||||||
|
File systemCertFile;
|
||||||
|
boolean first = (systemCertFile = trustUsingUpdateCaCertificates(certFile)) != null;
|
||||||
|
boolean second = trustUsingTrustAnchor(systemCertFile, certFile);
|
||||||
|
success = first || second;
|
||||||
|
} else if(certType == USER) {
|
||||||
|
// Install certificate to local profile using "certutil"
|
||||||
|
for(String nssdb : NSSDB_URLS) {
|
||||||
|
String[] parts = nssdb.split(":", 2);
|
||||||
|
if (parts.length > 1) {
|
||||||
|
File folder = new File(parts[1]);
|
||||||
|
// If .pki/nssdb doesn't exist yet, don't create it! Per https://github.com/qzind/tray/issues/1003
|
||||||
|
if(folder.exists() && folder.isDirectory()) {
|
||||||
|
if (!ShellUtilities.execute("certutil", "-d", nssdb, "-A", "-t", "TC", "-n", Constants.ABOUT_COMPANY, "-i", certFile.getPath())) {
|
||||||
|
log.warn("Something went wrong creating {}. HTTPS will fail on certain browsers which depend on it.", nssdb);
|
||||||
|
success = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean findCertutil() {
|
||||||
|
boolean installed = ShellUtilities.execute("which", "certutil");
|
||||||
|
if (!installed) {
|
||||||
|
if (certType == SYSTEM && promptCertutil()) {
|
||||||
|
if(UnixUtilities.isUbuntu() || UnixUtilities.isDebian()) {
|
||||||
|
installed = ShellUtilities.execute("apt-get", "install", "-y", "libnss3-tools");
|
||||||
|
} else if(UnixUtilities.isFedora()) {
|
||||||
|
installed = ShellUtilities.execute("dnf", "install", "-y", "nss-tools");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(!installed) {
|
||||||
|
log.warn("A critical component, \"certutil\" wasn't found and cannot be installed automatically. HTTPS will fail on certain browsers which depend on it.");
|
||||||
|
}
|
||||||
|
return installed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean promptCertutil() {
|
||||||
|
// Assume silent or headless installs want certutil
|
||||||
|
if(Installer.IS_SILENT || GraphicsEnvironment.isHeadless()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
SystemUtilities.setSystemLookAndFeel(true);
|
||||||
|
return JOptionPane.YES_OPTION == JOptionPane.showConfirmDialog(null, "A critical component, \"certutil\" wasn't found. Attempt to fetch it now?");
|
||||||
|
} catch(Throwable ignore) {}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Common technique for installing system-wide certificates on Debian-based systems (Ubuntu, etc.)
|
||||||
|
*
|
||||||
|
* This technique is only known to work for select browsers, such as Epiphany. Browsers such as
|
||||||
|
* Firefox and Chromium require different techniques.
|
||||||
|
*
|
||||||
|
* @return Full path to the destination file if successful, otherwise <code>null</code>
|
||||||
|
*/
|
||||||
|
private File trustUsingUpdateCaCertificates(File certFile) {
|
||||||
|
if(hasUpdateCaCertificatesCommand()) {
|
||||||
|
File destFile = new File(CA_CERTIFICATES, CA_CERTIFICATE_NAME);
|
||||||
|
log.debug("Copying SYSTEM SSL certificate {} to {}", certFile.getPath(), destFile.getPath());
|
||||||
|
try {
|
||||||
|
if (new File(CA_CERTIFICATES).isDirectory()) {
|
||||||
|
// Note: preserveFileDate=false per https://github.com/qzind/tray/issues/1011
|
||||||
|
FileUtils.copyFile(certFile, destFile, false);
|
||||||
|
if (destFile.isFile()) {
|
||||||
|
// Attempt "update-ca-certificates" (Debian)
|
||||||
|
if (!ShellUtilities.execute("update-ca-certificates")) {
|
||||||
|
log.warn("Something went wrong calling \"update-ca-certificates\" for the SYSTEM SSL certificate.");
|
||||||
|
} else {
|
||||||
|
return destFile;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.warn("{} is not a valid directory, skipping", CA_CERTIFICATES);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(IOException e) {
|
||||||
|
log.warn("Error copying SYSTEM SSL certificate file", e);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.warn("Skipping SYSTEM SSL certificate install using \"update-ca-certificates\", command missing or invalid");
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Common technique for installing system-wide certificates on Fedora-based systems
|
||||||
|
*
|
||||||
|
* Uses first existing non-null file provided
|
||||||
|
*/
|
||||||
|
private boolean trustUsingTrustAnchor(File ... certFiles) {
|
||||||
|
if (hasTrustAnchorCommand()) {
|
||||||
|
for(File certFile : certFiles) {
|
||||||
|
if (certFile == null || !certFile.exists()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Install certificate to system using "trust anchor" (Fedora)
|
||||||
|
if (ShellUtilities.execute("trust", "anchor", "--store", certFile.getPath())) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
log.warn("Something went wrong calling \"trust anchor\" for the SYSTEM SSL certificate.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.warn("Skipping SYSTEM SSL certificate install using \"trust anchor\", command missing or invalid");
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean distrustUsingUpdateCaCertificates(List<String> paths) {
|
||||||
|
if(hasUpdateCaCertificatesCommand()) {
|
||||||
|
boolean deleted = false;
|
||||||
|
for(String path : paths) {
|
||||||
|
// Process files only; not "trust anchor" URIs
|
||||||
|
if(!path.startsWith(PK11_KIT_ID)) {
|
||||||
|
File certFile = new File(path);
|
||||||
|
if (certFile.isFile() && certFile.delete()) {
|
||||||
|
deleted = true;
|
||||||
|
} else {
|
||||||
|
log.warn("SYSTEM SSL certificate {} does not exist, skipping", certFile.getPath());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Attempt "update-ca-certificates" (Debian)
|
||||||
|
if(deleted) {
|
||||||
|
if (ShellUtilities.execute("update-ca-certificates")) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
log.warn("Something went wrong calling \"update-ca-certificates\" for the SYSTEM SSL certificate.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.warn("Skipping SYSTEM SSL certificate removal using \"update-ca-certificates\", command missing or invalid");
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean distrustUsingTrustAnchor(List<String> idList) {
|
||||||
|
if(hasTrustAnchorCommand()) {
|
||||||
|
for(String id : idList) {
|
||||||
|
// only remove by id
|
||||||
|
if (id.startsWith(PK11_KIT_ID) && !ShellUtilities.execute("trust", "anchor", "--remove", id)) {
|
||||||
|
log.warn("Something went wrong calling \"trust anchor\" for the SYSTEM SSL certificate.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.warn("Skipping SYSTEM SSL certificate removal using \"trust anchor\", command missing or invalid");
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check for the presence of a QZ certificate in known locations (e.g. /usr/local/share/ca-certificates/
|
||||||
|
* and return the path if found
|
||||||
|
*/
|
||||||
|
private ArrayList<String> findUsingUsingUpdateCaCert() {
|
||||||
|
ArrayList<String> found = new ArrayList<>();
|
||||||
|
File[] systemCertFiles = { new File(CA_CERTIFICATES, CA_CERTIFICATE_NAME) };
|
||||||
|
for(File file : systemCertFiles) {
|
||||||
|
if(file.isFile()) {
|
||||||
|
found.add(file.getPath());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find QZ installed certificates in the "trust anchor" by searching by email.
|
||||||
|
*
|
||||||
|
* The "trust" utility identifies certificates as URIs:
|
||||||
|
* Example:
|
||||||
|
* pkcs11:id=%7C%5D%02%84%13%D4%CC%8A%9B%81%CE%17%1C%2E%29%1E%9C%48%63%42;type=cert
|
||||||
|
* ... which is an encoded version of the cert's SubjectKeyIdentifier field
|
||||||
|
* To identify a match:
|
||||||
|
* 1. Extract all trusted certificates and look for a familiar email address
|
||||||
|
* 2. If found, construct and store a "trust" compatible URI as the nickname
|
||||||
|
*/
|
||||||
|
private ArrayList<String> findUsingTrustAnchor() {
|
||||||
|
ArrayList<String> uris = new ArrayList<>();
|
||||||
|
File tempFile = null;
|
||||||
|
try {
|
||||||
|
// Temporary location for system certificates
|
||||||
|
tempFile = File.createTempFile("trust-extract-for-qz-", ".pem");
|
||||||
|
// Delete before use: "trust extract" requires an empty file
|
||||||
|
tempFile.delete();
|
||||||
|
if(ShellUtilities.execute("trust", "extract", "--format", "pem-bundle", tempFile.getPath())) {
|
||||||
|
BufferedReader reader = new BufferedReader(new FileReader(tempFile));
|
||||||
|
String line;
|
||||||
|
StringBuilder base64 = new StringBuilder();
|
||||||
|
while ((line = reader.readLine()) != null) {
|
||||||
|
if(line.startsWith(X509Constants.BEGIN_CERT)) {
|
||||||
|
// Beginning of a new certificate
|
||||||
|
base64.setLength(0);
|
||||||
|
} else if(line.startsWith(X509Constants.END_CERT)) {
|
||||||
|
// End of the existing certificate
|
||||||
|
byte[] certBytes = Base64.decode(base64.toString());
|
||||||
|
CertificateFactory factory = CertificateFactory.getInstance("X.509");
|
||||||
|
X509Certificate cert = (X509Certificate)factory.generateCertificate(new ByteArrayInputStream(certBytes));
|
||||||
|
if(CertificateManager.emailMatches(cert, true)) {
|
||||||
|
byte[] extensionValue = cert.getExtensionValue(Extension.subjectKeyIdentifier.getId());
|
||||||
|
byte[] octets = DEROctetString.getInstance(extensionValue).getOctets();
|
||||||
|
SubjectKeyIdentifier subjectKeyIdentifier = SubjectKeyIdentifier.getInstance(octets);
|
||||||
|
byte[] keyIdentifier = subjectKeyIdentifier.getKeyIdentifier();
|
||||||
|
String hex = ByteUtilities.bytesToHex(keyIdentifier, true);
|
||||||
|
String uri = PK11_KIT_ID + hex.replaceAll("(.{2})", "%$1") + ";type=cert";
|
||||||
|
log.info("Found matching cert: {}", uri);
|
||||||
|
|
||||||
|
uris.add(uri);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
base64.append(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reader.close();
|
||||||
|
}
|
||||||
|
} catch(IOException | CertificateException e) {
|
||||||
|
log.warn("An error occurred finding preexisting \"trust anchor\" certificates", e);
|
||||||
|
} finally {
|
||||||
|
if(tempFile != null && !tempFile.delete()) {
|
||||||
|
tempFile.deleteOnExit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return uris;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean hasUpdateCaCertificatesCommand() {
|
||||||
|
return ShellUtilities.execute("which", "update-ca-certificates");
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean hasTrustAnchorCommand() {
|
||||||
|
return ShellUtilities.execute("trust", "anchor", "--help");
|
||||||
|
}
|
||||||
|
}
|
||||||
91
old code/tray/src/qz/installer/certificate/MacCertificateInstaller.java
Executable file
91
old code/tray/src/qz/installer/certificate/MacCertificateInstaller.java
Executable file
@@ -0,0 +1,91 @@
|
|||||||
|
/**
|
||||||
|
* @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 qz.installer.certificate;
|
||||||
|
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
import qz.common.Constants;
|
||||||
|
import qz.installer.Installer;
|
||||||
|
import qz.utils.ShellUtilities;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class MacCertificateInstaller extends NativeCertificateInstaller {
|
||||||
|
private static final Logger log = LogManager.getLogger(MacCertificateInstaller.class);
|
||||||
|
|
||||||
|
public static final String USER_STORE = System.getProperty("user.home") + "/Library/Keychains/login.keychain"; // aka login.keychain-db
|
||||||
|
public static final String SYSTEM_STORE = "/Library/Keychains/System.keychain";
|
||||||
|
private String certStore;
|
||||||
|
|
||||||
|
public MacCertificateInstaller(Installer.PrivilegeLevel certType) {
|
||||||
|
setInstallType(certType);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean add(File certFile) {
|
||||||
|
if (certStore.equals(USER_STORE)) {
|
||||||
|
// This will prompt the user
|
||||||
|
return ShellUtilities.execute("security", "add-trusted-cert", "-r", "trustRoot", "-k", certStore, certFile.getPath());
|
||||||
|
} else {
|
||||||
|
return ShellUtilities.execute("security", "add-trusted-cert", "-d", "-r", "trustRoot", "-k", certStore, certFile.getPath());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean remove(List<String> idList) {
|
||||||
|
boolean success = true;
|
||||||
|
for (String certId : idList) {
|
||||||
|
success = success && ShellUtilities.execute("security", "delete-certificate", "-Z", certId, certStore);
|
||||||
|
}
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> find() {
|
||||||
|
ArrayList<String> hashList = new ArrayList<>();
|
||||||
|
try {
|
||||||
|
Process p = Runtime.getRuntime().exec(new String[] {"security", "find-certificate", "-a", "-e", Constants.ABOUT_EMAIL, "-Z", certStore});
|
||||||
|
BufferedReader in = new BufferedReader(new InputStreamReader(p.getInputStream()));
|
||||||
|
String line;
|
||||||
|
while ((line = in.readLine()) != null) {
|
||||||
|
if (line.contains("SHA-1") && line.contains(":")) {
|
||||||
|
hashList.add(line.split(":", 2)[1].trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
in.close();
|
||||||
|
} catch(IOException e) {
|
||||||
|
log.warn("Could not get certificate list", e);
|
||||||
|
}
|
||||||
|
return hashList;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean verify(File certFile) {
|
||||||
|
return ShellUtilities.execute( "security", "verify-cert", "-c", certFile.getPath());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setInstallType(Installer.PrivilegeLevel type) {
|
||||||
|
if (type == Installer.PrivilegeLevel.USER) {
|
||||||
|
certStore = USER_STORE;
|
||||||
|
} else {
|
||||||
|
certStore = SYSTEM_STORE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Installer.PrivilegeLevel getInstallType() {
|
||||||
|
if (certStore == USER_STORE) {
|
||||||
|
return Installer.PrivilegeLevel.USER;
|
||||||
|
} else {
|
||||||
|
return Installer.PrivilegeLevel.SYSTEM;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
105
old code/tray/src/qz/installer/certificate/NativeCertificateInstaller.java
Executable file
105
old code/tray/src/qz/installer/certificate/NativeCertificateInstaller.java
Executable file
@@ -0,0 +1,105 @@
|
|||||||
|
/**
|
||||||
|
* @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 qz.installer.certificate;
|
||||||
|
|
||||||
|
import org.bouncycastle.openssl.jcajce.JcaMiscPEMGenerator;
|
||||||
|
import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
import qz.installer.Installer;
|
||||||
|
import qz.utils.SystemUtilities;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStreamWriter;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.StandardOpenOption;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public abstract class NativeCertificateInstaller {
|
||||||
|
private static final Logger log = LogManager.getLogger(NativeCertificateInstaller.class);
|
||||||
|
protected static NativeCertificateInstaller instance;
|
||||||
|
|
||||||
|
public static NativeCertificateInstaller getInstance() {
|
||||||
|
return getInstance(SystemUtilities.isAdmin() ? Installer.PrivilegeLevel.SYSTEM : Installer.PrivilegeLevel.USER);
|
||||||
|
}
|
||||||
|
public static NativeCertificateInstaller getInstance(Installer.PrivilegeLevel type) {
|
||||||
|
if (instance == null) {
|
||||||
|
switch(SystemUtilities.getOs()) {
|
||||||
|
case WINDOWS:
|
||||||
|
instance = new WindowsCertificateInstaller(type);
|
||||||
|
break;
|
||||||
|
case MAC:
|
||||||
|
instance = new MacCertificateInstaller(type);
|
||||||
|
break;
|
||||||
|
case LINUX:
|
||||||
|
default:
|
||||||
|
instance = new LinuxCertificateInstaller(type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Install a certificate from memory
|
||||||
|
*/
|
||||||
|
public boolean install(X509Certificate cert) {
|
||||||
|
File certFile = null;
|
||||||
|
try {
|
||||||
|
certFile = File.createTempFile(KeyPairWrapper.getAlias(KeyPairWrapper.Type.CA) + "-", CertificateManager.DEFAULT_CERTIFICATE_EXTENSION);
|
||||||
|
JcaMiscPEMGenerator generator = new JcaMiscPEMGenerator(cert);
|
||||||
|
JcaPEMWriter writer = new JcaPEMWriter(new OutputStreamWriter(Files.newOutputStream(certFile.toPath(), StandardOpenOption.CREATE)));
|
||||||
|
writer.writeObject(generator.generate());
|
||||||
|
writer.close();
|
||||||
|
|
||||||
|
return install(certFile);
|
||||||
|
} catch(IOException e) {
|
||||||
|
log.warn("Could not install cert from temp file", e);
|
||||||
|
} finally {
|
||||||
|
if(certFile != null && !certFile.delete()) {
|
||||||
|
certFile.deleteOnExit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Install a certificate from disk
|
||||||
|
*/
|
||||||
|
public boolean install(File certFile) {
|
||||||
|
String helper = instance.getClass().getSimpleName();
|
||||||
|
String store = instance.getInstallType().name();
|
||||||
|
if(SystemUtilities.isJar()) {
|
||||||
|
if (remove(find())) {
|
||||||
|
log.info("Certificate removed from {} store using {}", store, helper);
|
||||||
|
} else {
|
||||||
|
log.warn("Could not remove certificate from {} store using {}", store, helper);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.info("Skipping {} store certificate removal, IDE detected.", store, helper);
|
||||||
|
}
|
||||||
|
if (add(certFile)) {
|
||||||
|
log.info("Certificate added to {} store using {}", store, helper);
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
log.warn("Could not install certificate to {} store using {}", store, helper);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract boolean add(File certFile);
|
||||||
|
public abstract boolean remove(List<String> idList);
|
||||||
|
public abstract List<String> find();
|
||||||
|
public abstract boolean verify(File certFile);
|
||||||
|
public abstract void setInstallType(Installer.PrivilegeLevel certType);
|
||||||
|
public abstract Installer.PrivilegeLevel getInstallType();
|
||||||
|
}
|
||||||
236
old code/tray/src/qz/installer/certificate/WindowsCertificateInstaller.java
Executable file
236
old code/tray/src/qz/installer/certificate/WindowsCertificateInstaller.java
Executable file
@@ -0,0 +1,236 @@
|
|||||||
|
/**
|
||||||
|
* @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 qz.installer.certificate;
|
||||||
|
|
||||||
|
import com.sun.jna.Memory;
|
||||||
|
import com.sun.jna.Native;
|
||||||
|
import com.sun.jna.Pointer;
|
||||||
|
import com.sun.jna.Structure;
|
||||||
|
import com.sun.jna.platform.win32.Kernel32Util;
|
||||||
|
import com.sun.jna.platform.win32.WinNT;
|
||||||
|
import com.sun.jna.win32.StdCallLibrary;
|
||||||
|
import com.sun.jna.win32.W32APIOptions;
|
||||||
|
import org.bouncycastle.cert.X509CertificateHolder;
|
||||||
|
import org.bouncycastle.openssl.PEMParser;
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
import qz.common.Constants;
|
||||||
|
import qz.installer.Installer;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class WindowsCertificateInstaller extends NativeCertificateInstaller {
|
||||||
|
private static final Logger log = LogManager.getLogger(WindowsCertificateInstaller.class);
|
||||||
|
private WinCrypt.HCERTSTORE store;
|
||||||
|
private byte[] certBytes;
|
||||||
|
private Installer.PrivilegeLevel certType;
|
||||||
|
|
||||||
|
public WindowsCertificateInstaller(Installer.PrivilegeLevel certType) {
|
||||||
|
setInstallType(certType);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean add(File certFile) {
|
||||||
|
log.info("Writing certificate {} to {} store using Crypt32...", certFile, certType);
|
||||||
|
try {
|
||||||
|
|
||||||
|
byte[] bytes = getCertBytes(certFile);
|
||||||
|
Pointer pointer = new Memory(bytes.length);
|
||||||
|
pointer.write(0, bytes, 0, bytes.length);
|
||||||
|
|
||||||
|
boolean success = Crypt32.INSTANCE.CertAddEncodedCertificateToStore(
|
||||||
|
openStore(),
|
||||||
|
WinCrypt.X509_ASN_ENCODING,
|
||||||
|
pointer,
|
||||||
|
bytes.length,
|
||||||
|
Crypt32.CERT_STORE_ADD_REPLACE_EXISTING,
|
||||||
|
null
|
||||||
|
);
|
||||||
|
if(!success) {
|
||||||
|
log.warn(Kernel32Util.formatMessage(Native.getLastError()));
|
||||||
|
}
|
||||||
|
|
||||||
|
closeStore();
|
||||||
|
|
||||||
|
return success;
|
||||||
|
} catch(IOException e) {
|
||||||
|
log.warn("An error occurred installing the certificate", e);
|
||||||
|
} finally {
|
||||||
|
certBytes = null;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] getCertBytes(File certFile) throws IOException {
|
||||||
|
if(certBytes == null) {
|
||||||
|
PEMParser pem = new PEMParser(new FileReader(certFile));
|
||||||
|
X509CertificateHolder certHolder = (X509CertificateHolder)pem.readObject();
|
||||||
|
certBytes = certHolder.getEncoded();
|
||||||
|
}
|
||||||
|
return certBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private WinCrypt.HCERTSTORE openStore() {
|
||||||
|
if(store == null) {
|
||||||
|
store = openStore(certType);
|
||||||
|
}
|
||||||
|
return store;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void closeStore() {
|
||||||
|
if(store != null && closeStore(store)) {
|
||||||
|
store = null;
|
||||||
|
} else {
|
||||||
|
log.warn("Unable to close {} cert store", certType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static WinCrypt.HCERTSTORE openStore(Installer.PrivilegeLevel certType) {
|
||||||
|
log.info("Opening {} store using Crypt32...", certType);
|
||||||
|
|
||||||
|
WinCrypt.HCERTSTORE store = Crypt32.INSTANCE.CertOpenStore(
|
||||||
|
Crypt32.CERT_STORE_PROV_SYSTEM,
|
||||||
|
0,
|
||||||
|
null,
|
||||||
|
certType == Installer.PrivilegeLevel.USER ? Crypt32.CERT_SYSTEM_STORE_CURRENT_USER : Crypt32.CERT_SYSTEM_STORE_LOCAL_MACHINE,
|
||||||
|
"ROOT"
|
||||||
|
);
|
||||||
|
if(store == null) {
|
||||||
|
log.warn(Kernel32Util.formatMessage(Native.getLastError()));
|
||||||
|
}
|
||||||
|
return store;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean closeStore(WinCrypt.HCERTSTORE certStore) {
|
||||||
|
boolean isClosed = Crypt32.INSTANCE.CertCloseStore(
|
||||||
|
certStore, 0
|
||||||
|
);
|
||||||
|
if(!isClosed) {
|
||||||
|
log.warn(Kernel32Util.formatMessage(Native.getLastError()));
|
||||||
|
}
|
||||||
|
return isClosed;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean remove(List<String> ignore) {
|
||||||
|
boolean success = true;
|
||||||
|
|
||||||
|
WinCrypt.CERT_CONTEXT hCertContext;
|
||||||
|
WinCrypt.CERT_CONTEXT pPrevCertContext = null;
|
||||||
|
while(true) {
|
||||||
|
hCertContext = Crypt32.INSTANCE.CertFindCertificateInStore(
|
||||||
|
openStore(),
|
||||||
|
WinCrypt.X509_ASN_ENCODING,
|
||||||
|
0,
|
||||||
|
Crypt32.CERT_FIND_SUBJECT_STR,
|
||||||
|
Constants.ABOUT_EMAIL,
|
||||||
|
pPrevCertContext);
|
||||||
|
|
||||||
|
if(hCertContext == null) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
pPrevCertContext = Crypt32.INSTANCE.CertDuplicateCertificateContext(hCertContext);
|
||||||
|
|
||||||
|
if(success = (success && Crypt32.INSTANCE.CertDeleteCertificateFromStore(hCertContext))) {
|
||||||
|
log.info("Successfully deleted certificate matching {}", Constants.ABOUT_EMAIL);
|
||||||
|
} else {
|
||||||
|
log.info("Could not delete certificate: {}", Kernel32Util.formatMessage(Native.getLastError()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
closeStore();
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> find() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setInstallType(Installer.PrivilegeLevel type) {
|
||||||
|
this.certType = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Installer.PrivilegeLevel getInstallType() {
|
||||||
|
return certType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean verify(File certFile) {
|
||||||
|
try {
|
||||||
|
MessageDigest md = MessageDigest.getInstance("SHA-1");
|
||||||
|
md.update(getCertBytes(certFile));
|
||||||
|
WinCrypt.DATA_BLOB thumbPrint = new WinCrypt.DATA_BLOB(md.digest());
|
||||||
|
WinNT.HANDLE cert = Crypt32.INSTANCE.CertFindCertificateInStore(
|
||||||
|
openStore(),
|
||||||
|
WinCrypt.X509_ASN_ENCODING,
|
||||||
|
0,
|
||||||
|
Crypt32.CERT_FIND_SHA1_HASH,
|
||||||
|
thumbPrint,
|
||||||
|
null);
|
||||||
|
|
||||||
|
return cert != null;
|
||||||
|
} catch(IOException | NoSuchAlgorithmException e) {
|
||||||
|
log.warn("An error occurred verifying the cert is installed: {}", certFile, e);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The JNA's Crypt32 instance oversimplifies store handling, preventing user stores from being used
|
||||||
|
*/
|
||||||
|
interface Crypt32 extends StdCallLibrary {
|
||||||
|
int CERT_SYSTEM_STORE_CURRENT_USER = 65536;
|
||||||
|
int CERT_SYSTEM_STORE_LOCAL_MACHINE = 131072;
|
||||||
|
int CERT_STORE_PROV_SYSTEM = 10;
|
||||||
|
int CERT_STORE_ADD_REPLACE_EXISTING = 3;
|
||||||
|
int CERT_FIND_SUBJECT_STR = 524295;
|
||||||
|
int CERT_FIND_SHA1_HASH = 65536;
|
||||||
|
|
||||||
|
Crypt32 INSTANCE = Native.load("Crypt32", Crypt32.class, W32APIOptions.DEFAULT_OPTIONS);
|
||||||
|
|
||||||
|
WinCrypt.HCERTSTORE CertOpenStore(int lpszStoreProvider, int dwMsgAndCertEncodingType, Pointer hCryptProv, int dwFlags, String pvPara);
|
||||||
|
boolean CertCloseStore(WinCrypt.HCERTSTORE hCertStore, int dwFlags);
|
||||||
|
boolean CertAddEncodedCertificateToStore(WinCrypt.HCERTSTORE hCertStore, int dwCertEncodingType, Pointer pbCertEncoded, int cbCertEncoded, int dwAddDisposition, Pointer ppCertContext);
|
||||||
|
WinCrypt.CERT_CONTEXT CertFindCertificateInStore (WinCrypt.HCERTSTORE hCertStore, int dwCertEncodingType, int dwFindFlags, int dwFindType, String pvFindPara, WinCrypt.CERT_CONTEXT pPrevCertContext);
|
||||||
|
WinCrypt.CERT_CONTEXT CertFindCertificateInStore (WinCrypt.HCERTSTORE hCertStore, int dwCertEncodingType, int dwFindFlags, int dwFindType, Structure pvFindPara, WinCrypt.CERT_CONTEXT pPrevCertContext);
|
||||||
|
boolean CertDeleteCertificateFromStore(WinCrypt.CERT_CONTEXT pCertContext);
|
||||||
|
boolean CertFreeCertificateContext(WinCrypt.CERT_CONTEXT pCertContext);
|
||||||
|
WinCrypt.CERT_CONTEXT CertDuplicateCertificateContext(WinCrypt.CERT_CONTEXT pCertContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Polyfill from JNA5+
|
||||||
|
@SuppressWarnings("UnusedDeclaration") //Library class
|
||||||
|
public static class WinCrypt {
|
||||||
|
public static int X509_ASN_ENCODING = 0x00000001;
|
||||||
|
public static class HCERTSTORE extends WinNT.HANDLE {
|
||||||
|
public HCERTSTORE() {}
|
||||||
|
public HCERTSTORE(Pointer p) {
|
||||||
|
super(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public static class CERT_CONTEXT extends WinNT.HANDLE {
|
||||||
|
public CERT_CONTEXT() {}
|
||||||
|
public CERT_CONTEXT(Pointer p) {
|
||||||
|
super(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public static class DATA_BLOB extends com.sun.jna.platform.win32.WinCrypt.DATA_BLOB {
|
||||||
|
// Wrap the constructor for code readability
|
||||||
|
public DATA_BLOB() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
public DATA_BLOB(byte[] data) {
|
||||||
|
super(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
136
old code/tray/src/qz/installer/certificate/WindowsCertificateInstallerCli.java
Executable file
136
old code/tray/src/qz/installer/certificate/WindowsCertificateInstallerCli.java
Executable file
@@ -0,0 +1,136 @@
|
|||||||
|
/**
|
||||||
|
* @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 qz.installer.certificate;
|
||||||
|
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
import qz.common.Constants;
|
||||||
|
import qz.installer.Installer;
|
||||||
|
import qz.utils.ShellUtilities;
|
||||||
|
import qz.utils.WindowsUtilities;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Command Line technique for installing certificates on Windows
|
||||||
|
* Fallback class for when JNA is not available (e.g. Windows on ARM)
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("UnusedDeclaration") //Library class
|
||||||
|
public class WindowsCertificateInstallerCli extends NativeCertificateInstaller {
|
||||||
|
private static final Logger log = LogManager.getLogger(WindowsCertificateInstallerCli.class);
|
||||||
|
private Installer.PrivilegeLevel certType;
|
||||||
|
|
||||||
|
public WindowsCertificateInstallerCli(Installer.PrivilegeLevel certType) {
|
||||||
|
setInstallType(certType);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean add(File certFile) {
|
||||||
|
if (WindowsUtilities.isWindowsXP()) return false;
|
||||||
|
if (certType == Installer.PrivilegeLevel.USER) {
|
||||||
|
// This will prompt the user
|
||||||
|
return ShellUtilities.execute("certutil.exe", "-addstore", "-f", "-user", "Root", certFile.getPath());
|
||||||
|
} else {
|
||||||
|
return ShellUtilities.execute("certutil.exe", "-addstore", "-f", "Root", certFile.getPath());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean remove(List<String> idList) {
|
||||||
|
if (WindowsUtilities.isWindowsXP()) return false;
|
||||||
|
boolean success = true;
|
||||||
|
for (String certId : idList) {
|
||||||
|
if (certType == Installer.PrivilegeLevel.USER) {
|
||||||
|
success = success && ShellUtilities.execute("certutil.exe", "-delstore", "-user", "Root", certId);
|
||||||
|
} else {
|
||||||
|
success = success && ShellUtilities.execute("certutil.exe", "-delstore", "Root", certId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a list of serials, if found
|
||||||
|
*/
|
||||||
|
public List<String> find() {
|
||||||
|
ArrayList<String> serialList = new ArrayList<>();
|
||||||
|
try {
|
||||||
|
Process p;
|
||||||
|
if (certType == Installer.PrivilegeLevel.USER) {
|
||||||
|
p = Runtime.getRuntime().exec(new String[] {"certutil.exe", "-store", "-user", "Root"});
|
||||||
|
} else {
|
||||||
|
p = Runtime.getRuntime().exec(new String[] {"certutil.exe", "-store", "Root"});
|
||||||
|
}
|
||||||
|
BufferedReader in = new BufferedReader(new InputStreamReader(p.getInputStream()));
|
||||||
|
String line;
|
||||||
|
while ((line = in.readLine()) != null) {
|
||||||
|
if (line.contains("================")) {
|
||||||
|
// First line is serial
|
||||||
|
String serial = parseNextLine(in);
|
||||||
|
if (serial != null) {
|
||||||
|
// Second line is issuer
|
||||||
|
String issuer = parseNextLine(in);
|
||||||
|
if (issuer.contains("OU=" + Constants.ABOUT_COMPANY)) {
|
||||||
|
serialList.add(serial);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
in.close();
|
||||||
|
} catch(Exception e) {
|
||||||
|
log.info("Unable to find a Trusted Root Certificate matching \"OU={}\"", Constants.ABOUT_COMPANY);
|
||||||
|
}
|
||||||
|
return serialList;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean verify(File certFile) {
|
||||||
|
return verifyCert(certFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean verifyCert(File certFile) {
|
||||||
|
// -user also will check the root store
|
||||||
|
String dwErrorStatus = ShellUtilities.execute( new String[] {"certutil", "-user", "-verify", certFile.getPath() }, new String[] { "dwErrorStatus=" }, false, false);
|
||||||
|
if(!dwErrorStatus.isEmpty()) {
|
||||||
|
String[] parts = dwErrorStatus.split("[\r\n\\s]+");
|
||||||
|
for(String part : parts) {
|
||||||
|
if(part.startsWith("dwErrorStatus=")) {
|
||||||
|
log.info("Certificate validity says {}", part);
|
||||||
|
String[] status = part.split("=", 2);
|
||||||
|
if (status.length == 2) {
|
||||||
|
return status[1].trim().equals("0");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.warn("Unable to determine certificate validity, you'll be prompted on startup");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setInstallType(Installer.PrivilegeLevel type) {
|
||||||
|
this.certType = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Installer.PrivilegeLevel getInstallType() {
|
||||||
|
return certType;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String parseNextLine(BufferedReader reader) throws IOException {
|
||||||
|
String data = reader.readLine();
|
||||||
|
if (data != null) {
|
||||||
|
String[] split = data.split(":", 2);
|
||||||
|
if (split.length == 2) {
|
||||||
|
return split[1].trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package qz.installer.certificate.firefox;
|
||||||
|
|
||||||
|
class ConflictingPolicyException extends Exception {
|
||||||
|
ConflictingPolicyException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,282 @@
|
|||||||
|
/**
|
||||||
|
* @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 qz.installer.certificate.firefox;
|
||||||
|
|
||||||
|
import com.github.zafarkhaja.semver.Version;
|
||||||
|
import com.sun.jna.platform.win32.WinReg;
|
||||||
|
import org.codehaus.jettison.json.JSONException;
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
import qz.common.Constants;
|
||||||
|
import qz.installer.Installer;
|
||||||
|
import qz.installer.certificate.CertificateManager;
|
||||||
|
import qz.installer.certificate.firefox.locator.AppAlias;
|
||||||
|
import qz.installer.certificate.firefox.locator.AppInfo;
|
||||||
|
import qz.installer.certificate.firefox.locator.AppLocator;
|
||||||
|
import qz.utils.JsonWriter;
|
||||||
|
import qz.utils.ShellUtilities;
|
||||||
|
import qz.utils.SystemUtilities;
|
||||||
|
import qz.utils.WindowsUtilities;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.security.cert.CertificateEncodingException;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Base64;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Installs the Firefox Policy file via Enterprise Policy, Distribution Policy file or AutoConfig, depending on OS & version
|
||||||
|
*/
|
||||||
|
public class FirefoxCertificateInstaller {
|
||||||
|
protected static final Logger log = LogManager.getLogger(FirefoxCertificateInstaller.class);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Versions are for Mozilla's official Firefox release.
|
||||||
|
* 3rd-party/clones may adopt Enterprise Policy support under
|
||||||
|
* different version numbers, adapt as needed.
|
||||||
|
*/
|
||||||
|
private static final Version WINDOWS_POLICY_VERSION = Version.valueOf("62.0.0");
|
||||||
|
private static final Version MAC_POLICY_VERSION = Version.valueOf("63.0.0");
|
||||||
|
private static final Version LINUX_POLICY_VERSION = Version.valueOf("65.0.0");
|
||||||
|
public static final Version FIREFOX_RESTART_VERSION = Version.valueOf("60.0.0");
|
||||||
|
|
||||||
|
public static final String LINUX_GLOBAL_POLICY_LOCATION = "/etc/firefox/policies/policies.json";
|
||||||
|
public static final String LINUX_SNAP_CERT_LOCATION = "/etc/firefox/policies/" + Constants.PROPS_FILE + CertificateManager.DEFAULT_CERTIFICATE_EXTENSION; // See https://github.com/mozilla/policy-templates/issues/936
|
||||||
|
public static final String LINUX_GLOBAL_CERT_LOCATION = "/usr/lib/mozilla/certificates/" + Constants.PROPS_FILE + CertificateManager.DEFAULT_CERTIFICATE_EXTENSION;
|
||||||
|
private static String DISTRIBUTION_ENTERPRISE_ROOT_POLICY = "{ \"policies\": { \"Certificates\": { \"ImportEnterpriseRoots\": true } } }";
|
||||||
|
private static String DISTRIBUTION_INSTALL_CERT_POLICY = "{ \"policies\": { \"Certificates\": { \"Install\": [ \"" + Constants.PROPS_FILE + CertificateManager.DEFAULT_CERTIFICATE_EXTENSION + "\", \"" + LINUX_SNAP_CERT_LOCATION + "\" ] } } }";
|
||||||
|
private static String DISTRIBUTION_REMOVE_CERT_POLICY = "{ \"policies\": { \"Certificates\": { \"Install\": [ \"/opt/" + Constants.PROPS_FILE + "/auth/root-ca.crt\"] } } }";
|
||||||
|
|
||||||
|
public static final String DISTRIBUTION_POLICY_LOCATION = "distribution/policies.json";
|
||||||
|
public static final String DISTRIBUTION_MAC_POLICY_LOCATION = "Contents/Resources/" + DISTRIBUTION_POLICY_LOCATION;
|
||||||
|
|
||||||
|
public static final String POLICY_AUDIT_MESSAGE = "Enterprise policy installed by " + Constants.ABOUT_TITLE + " on " + SystemUtilities.timeStamp();
|
||||||
|
|
||||||
|
public static void install(X509Certificate cert, String ... hostNames) {
|
||||||
|
// Blindly install Firefox enterprise policies to the system (macOS, Windows)
|
||||||
|
ArrayList<AppAlias.Alias> enterpriseFailed = new ArrayList<>();
|
||||||
|
for(AppAlias.Alias alias : AppAlias.FIREFOX.getAliases()) {
|
||||||
|
boolean success = false;
|
||||||
|
try {
|
||||||
|
if(alias.isEnterpriseReady() && !hasEnterprisePolicy(alias, false)) {
|
||||||
|
log.info("Installing Firefox enterprise certificate policy for {}", alias);
|
||||||
|
success = installEnterprisePolicy(alias, false);
|
||||||
|
}
|
||||||
|
} catch(ConflictingPolicyException e) {
|
||||||
|
log.warn("Conflict found installing {} enterprise cert support. We'll fallback on the distribution policy instead", alias.getName(), e);
|
||||||
|
}
|
||||||
|
if(!success) {
|
||||||
|
enterpriseFailed.add(alias);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search for installed instances
|
||||||
|
ArrayList<AppInfo> foundApps = AppLocator.getInstance().locate(AppAlias.FIREFOX);
|
||||||
|
ArrayList<Path> processPaths = null;
|
||||||
|
|
||||||
|
for(AppInfo appInfo : foundApps) {
|
||||||
|
boolean success = false;
|
||||||
|
if (honorsPolicy(appInfo)) {
|
||||||
|
if((SystemUtilities.isWindows()|| SystemUtilities.isMac()) && !enterpriseFailed.contains(appInfo.getAlias())) {
|
||||||
|
// Enterprise policy was already installed
|
||||||
|
success = true;
|
||||||
|
} else {
|
||||||
|
log.info("Installing Firefox distribution policy for {}", appInfo);
|
||||||
|
success = installDistributionPolicy(appInfo, cert);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.info("Installing Firefox auto-config script for {}", appInfo);
|
||||||
|
try {
|
||||||
|
String certData = Base64.getEncoder().encodeToString(cert.getEncoded());
|
||||||
|
success = LegacyFirefoxCertificateInstaller.installAutoConfigScript(appInfo, certData, hostNames);
|
||||||
|
}
|
||||||
|
catch(CertificateEncodingException e) {
|
||||||
|
log.warn("Unable to install auto-config script for {}", appInfo, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(success) {
|
||||||
|
issueRestartWarning(processPaths = AppLocator.getRunningPaths(foundApps, processPaths), appInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void uninstall() {
|
||||||
|
ArrayList<AppInfo> appList = AppLocator.getInstance().locate(AppAlias.FIREFOX);
|
||||||
|
for(AppInfo appInfo : appList) {
|
||||||
|
if(honorsPolicy(appInfo)) {
|
||||||
|
if(SystemUtilities.isWindows() || SystemUtilities.isMac()) {
|
||||||
|
log.info("Skipping uninstall of Firefox enterprise root certificate policy for {}", appInfo);
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
File policy = appInfo.getPath().resolve(DISTRIBUTION_POLICY_LOCATION).toFile();
|
||||||
|
if(policy.exists()) {
|
||||||
|
JsonWriter.write(appInfo.getPath().resolve(DISTRIBUTION_POLICY_LOCATION).toString(), DISTRIBUTION_INSTALL_CERT_POLICY, false, true);
|
||||||
|
}
|
||||||
|
} catch(IOException | JSONException e) {
|
||||||
|
log.warn("Unable to remove Firefox policy for {}", appInfo, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.info("Uninstalling Firefox auto-config script for {}", appInfo);
|
||||||
|
LegacyFirefoxCertificateInstaller.uninstallAutoConfigScript(appInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean honorsPolicy(AppInfo appInfo) {
|
||||||
|
if (appInfo.getVersion() == null) {
|
||||||
|
log.warn("Firefox-compatible browser found {}, but no version information is available", appInfo);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if(SystemUtilities.isWindows()) {
|
||||||
|
return appInfo.getVersion().greaterThanOrEqualTo(WINDOWS_POLICY_VERSION);
|
||||||
|
} else if (SystemUtilities.isMac()) {
|
||||||
|
return appInfo.getVersion().greaterThanOrEqualTo(MAC_POLICY_VERSION);
|
||||||
|
} else {
|
||||||
|
return appInfo.getVersion().greaterThanOrEqualTo(LINUX_POLICY_VERSION);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if an alternative Firefox policy (e.g. registry, plist user or system) is installed
|
||||||
|
*/
|
||||||
|
private static boolean hasEnterprisePolicy(AppAlias.Alias alias, boolean userOnly) throws ConflictingPolicyException {
|
||||||
|
if(SystemUtilities.isWindows()) {
|
||||||
|
String key = String.format("Software\\Policies\\%s\\%s\\Certificates", alias.getVendor(), alias.getName(true));
|
||||||
|
Integer foundPolicy = WindowsUtilities.getRegInt(userOnly ? WinReg.HKEY_CURRENT_USER : WinReg.HKEY_LOCAL_MACHINE, key, "ImportEnterpriseRoots");
|
||||||
|
if(foundPolicy != null) {
|
||||||
|
return foundPolicy == 1;
|
||||||
|
}
|
||||||
|
} else if(SystemUtilities.isMac()) {
|
||||||
|
String policyLocation = "/Library/Preferences/";
|
||||||
|
if(userOnly) {
|
||||||
|
policyLocation = System.getProperty("user.home") + policyLocation;
|
||||||
|
}
|
||||||
|
String policesEnabled = ShellUtilities.executeRaw(new String[] { "defaults", "read", policyLocation + alias.getBundleId(), "EnterprisePoliciesEnabled"}, true);
|
||||||
|
String foundPolicy = ShellUtilities.executeRaw(new String[] {"defaults", "read", policyLocation + alias.getBundleId(), "Certificates"}, true);
|
||||||
|
if(!policesEnabled.isEmpty() && !foundPolicy.isEmpty()) {
|
||||||
|
// Policies exist, decide how to proceed
|
||||||
|
if(policesEnabled.trim().equals("1") && foundPolicy.contains("ImportEnterpriseRoots = 1;")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
throw new ConflictingPolicyException(String.format("%s enterprise policy conflict at %s: %s", alias.getName(), policyLocation + alias.getBundleId(), foundPolicy));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Linux alternate policy not yet supported
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Install policy to distribution/policies.json
|
||||||
|
*/
|
||||||
|
public static boolean installDistributionPolicy(AppInfo app, X509Certificate cert) {
|
||||||
|
Path jsonPath = app.getPath().resolve(SystemUtilities.isMac() ? DISTRIBUTION_MAC_POLICY_LOCATION:DISTRIBUTION_POLICY_LOCATION);
|
||||||
|
String jsonPolicy = SystemUtilities.isWindows() || SystemUtilities.isMac() ? DISTRIBUTION_ENTERPRISE_ROOT_POLICY:DISTRIBUTION_INSTALL_CERT_POLICY;
|
||||||
|
|
||||||
|
// Special handling for snaps
|
||||||
|
if(app.getPath().toString().startsWith("/snap")) {
|
||||||
|
log.info("Snap detected, installing policy file to global location instead: {}", LINUX_GLOBAL_POLICY_LOCATION);
|
||||||
|
jsonPath = Paths.get(LINUX_GLOBAL_POLICY_LOCATION);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if(jsonPolicy.equals(DISTRIBUTION_INSTALL_CERT_POLICY)) {
|
||||||
|
// Linux lacks the concept of "enterprise roots", we'll write it to a known location instead
|
||||||
|
writeCertFile(cert, LINUX_SNAP_CERT_LOCATION); // so that the snap can read from it
|
||||||
|
writeCertFile(cert, LINUX_GLOBAL_CERT_LOCATION); // default location for non-snaps
|
||||||
|
}
|
||||||
|
|
||||||
|
File jsonFile = jsonPath.toFile();
|
||||||
|
|
||||||
|
// Make sure we can traverse and read
|
||||||
|
File distribution = jsonFile.getParentFile();
|
||||||
|
distribution.mkdirs();
|
||||||
|
distribution.setReadable(true, false);
|
||||||
|
distribution.setExecutable(true, false);
|
||||||
|
|
||||||
|
if(jsonPolicy.equals(DISTRIBUTION_INSTALL_CERT_POLICY)) {
|
||||||
|
// Delete previous policy
|
||||||
|
JsonWriter.write(jsonPath.toString(), DISTRIBUTION_REMOVE_CERT_POLICY, false, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonWriter.write(jsonPath.toString(), jsonPolicy, false, false);
|
||||||
|
|
||||||
|
// Make sure ew can read
|
||||||
|
jsonFile.setReadable(true, false);
|
||||||
|
return true;
|
||||||
|
} catch(JSONException | IOException e) {
|
||||||
|
log.warn("Could not install distribution policy {} to {}", jsonPolicy, jsonPath.toString(), e);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean installEnterprisePolicy(AppAlias.Alias alias, boolean userOnly) {
|
||||||
|
if(SystemUtilities.isWindows()) {
|
||||||
|
String key = String.format("Software\\Policies\\%s\\%s\\Certificates", alias.getVendor(), alias.getName(true));;
|
||||||
|
WindowsUtilities.addRegValue(userOnly ? WinReg.HKEY_CURRENT_USER : WinReg.HKEY_LOCAL_MACHINE, key, "Comment", POLICY_AUDIT_MESSAGE);
|
||||||
|
return WindowsUtilities.addRegValue(userOnly ? WinReg.HKEY_CURRENT_USER : WinReg.HKEY_LOCAL_MACHINE, key, "ImportEnterpriseRoots", 1);
|
||||||
|
} else if(SystemUtilities.isMac()) {
|
||||||
|
String policyLocation = "/Library/Preferences/";
|
||||||
|
if(userOnly) {
|
||||||
|
policyLocation = System.getProperty("user.home") + policyLocation;
|
||||||
|
}
|
||||||
|
return ShellUtilities.execute(new String[] {"defaults", "write", policyLocation + alias.getBundleId(), "EnterprisePoliciesEnabled", "-bool", "TRUE"}, true) &&
|
||||||
|
ShellUtilities.execute(new String[] {"defaults", "write", policyLocation + alias.getBundleId(), "Certificates", "-dict", "ImportEnterpriseRoots", "-bool", "TRUE",
|
||||||
|
"Comment", "-string", POLICY_AUDIT_MESSAGE}, true);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean issueRestartWarning(ArrayList<Path> runningPaths, AppInfo appInfo) {
|
||||||
|
boolean firefoxIsRunning = runningPaths.contains(appInfo.getExePath());
|
||||||
|
|
||||||
|
// Edge case for detecting if snap is running, since we can't compare the exact path easily
|
||||||
|
for(Path runningPath : runningPaths) {
|
||||||
|
if(runningPath.startsWith("/snap/")) {
|
||||||
|
firefoxIsRunning = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (firefoxIsRunning) {
|
||||||
|
if (appInfo.getVersion().greaterThanOrEqualTo(FirefoxCertificateInstaller.FIREFOX_RESTART_VERSION)) {
|
||||||
|
try {
|
||||||
|
Installer.getInstance().spawn(appInfo.getExePath().toString(), "-private", "about:restartrequired");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch(Exception ignore) {}
|
||||||
|
} else {
|
||||||
|
log.warn("{} must be restarted manually for changes to take effect", appInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void writeCertFile(X509Certificate cert, String location) throws IOException {
|
||||||
|
File certFile = new File(location);
|
||||||
|
|
||||||
|
// Make sure we can traverse and read
|
||||||
|
File certs = new File(location).getParentFile();
|
||||||
|
certs.mkdirs();
|
||||||
|
certs.setReadable(true, false);
|
||||||
|
certs.setExecutable(true, false);
|
||||||
|
File mozilla = certs.getParentFile();
|
||||||
|
mozilla.setReadable(true, false);
|
||||||
|
mozilla.setExecutable(true, false);
|
||||||
|
|
||||||
|
// Make sure we can read
|
||||||
|
CertificateManager.writeCert(cert, certFile);
|
||||||
|
certFile.setReadable(true, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
/**
|
||||||
|
* @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 qz.installer.certificate.firefox;
|
||||||
|
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
import qz.common.Constants;
|
||||||
|
import qz.installer.certificate.CertificateChainBuilder;
|
||||||
|
import qz.installer.certificate.firefox.locator.AppInfo;
|
||||||
|
import qz.utils.FileUtilities;
|
||||||
|
import qz.utils.SystemUtilities;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.security.cert.CertificateEncodingException;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Legacy Firefox Certificate installer
|
||||||
|
*
|
||||||
|
* For old Firefox-compatible browsers still in the wild such as Firefox 52 ESR, SeaMonkey, WaterFox, etc.
|
||||||
|
*/
|
||||||
|
public class LegacyFirefoxCertificateInstaller {
|
||||||
|
private static final Logger log = LogManager.getLogger(CertificateChainBuilder.class);
|
||||||
|
|
||||||
|
private static final String CFG_TEMPLATE = "assets/firefox-autoconfig.js.in";
|
||||||
|
private static final String CFG_FILE = Constants.PROPS_FILE + ".cfg";
|
||||||
|
private static final String PREFS_FILE = Constants.PROPS_FILE + ".js";
|
||||||
|
private static final String PREFS_DIR = "defaults/pref";
|
||||||
|
private static final String MAC_PREFIX = "Contents/Resources";
|
||||||
|
|
||||||
|
public static boolean installAutoConfigScript(AppInfo appInfo, String certData, String ... hostNames) {
|
||||||
|
try {
|
||||||
|
if(appInfo.getPath().toString().equals("/usr/bin")) {
|
||||||
|
throw new Exception("Preventing install to root location");
|
||||||
|
}
|
||||||
|
writePrefsFile(appInfo);
|
||||||
|
writeParsedConfig(appInfo, certData, false, hostNames);
|
||||||
|
return true;
|
||||||
|
} catch(Exception e) {
|
||||||
|
log.warn("Error installing auto-config support for {}", appInfo, e);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean uninstallAutoConfigScript(AppInfo appInfo) {
|
||||||
|
try {
|
||||||
|
writeParsedConfig(appInfo, "", true);
|
||||||
|
return true;
|
||||||
|
} catch(Exception e) {
|
||||||
|
log.warn("Error uninstalling auto-config support for {}", appInfo, e);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static File tryWrite(AppInfo appInfo, boolean mkdirs, String ... paths) throws IOException {
|
||||||
|
Path dir = appInfo.getPath();
|
||||||
|
if (SystemUtilities.isMac()) {
|
||||||
|
dir = dir.resolve(MAC_PREFIX);
|
||||||
|
}
|
||||||
|
for (String path : paths) {
|
||||||
|
dir = dir.resolve(path);
|
||||||
|
}
|
||||||
|
File file = dir.toFile();
|
||||||
|
|
||||||
|
if(mkdirs) file.mkdirs();
|
||||||
|
if(file.exists() && file.isDirectory() && file.canWrite()) {
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new IOException(String.format("Directory does not exist or is not writable: %s", file));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void deleteFile(File parent, String ... paths) {
|
||||||
|
if(parent != null) {
|
||||||
|
String toDelete = parent.getPath();
|
||||||
|
for (String path : paths) {
|
||||||
|
toDelete += File.separator + path;
|
||||||
|
}
|
||||||
|
File deleteFile = new File(toDelete);
|
||||||
|
if (!deleteFile.exists()) {
|
||||||
|
} else if (new File(toDelete).delete()) {
|
||||||
|
log.info("Deleted old file: {}", toDelete);
|
||||||
|
} else {
|
||||||
|
log.warn("Could not delete old file: {}", toDelete);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void writePrefsFile(AppInfo app) throws Exception {
|
||||||
|
File prefsDir = tryWrite(app, true, PREFS_DIR);
|
||||||
|
deleteFile(prefsDir, "firefox-prefs.js"); // cleanup old version
|
||||||
|
|
||||||
|
// first check that there aren't other prefs files
|
||||||
|
String pref = "general.config.filename";
|
||||||
|
for (File file : prefsDir.listFiles()) {
|
||||||
|
try {
|
||||||
|
BufferedReader reader = new BufferedReader(new FileReader(file));
|
||||||
|
String line;
|
||||||
|
while((line = reader.readLine()) != null) {
|
||||||
|
if(line.contains(pref) && !line.contains(CFG_FILE)) {
|
||||||
|
throw new Exception(String.format("Browser already has %s defined in %s:\n %s", pref, file, line));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch(IOException ignore) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// write out the new prefs file
|
||||||
|
File prefsFile = new File(prefsDir, PREFS_FILE);
|
||||||
|
BufferedWriter writer = new BufferedWriter(new FileWriter(prefsFile));
|
||||||
|
String[] data = {
|
||||||
|
String.format("pref('%s', '%s');", pref, CFG_FILE),
|
||||||
|
"pref('general.config.obscure_value', 0);"
|
||||||
|
};
|
||||||
|
for (String line : data) {
|
||||||
|
writer.write(line + "\n");
|
||||||
|
}
|
||||||
|
writer.close();
|
||||||
|
prefsFile.setReadable(true, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void writeParsedConfig(AppInfo appInfo, String certData, boolean uninstall, String ... hostNames) throws IOException, CertificateEncodingException{
|
||||||
|
if (hostNames.length == 0) hostNames = CertificateChainBuilder.DEFAULT_HOSTNAMES;
|
||||||
|
|
||||||
|
File cfgDir = tryWrite(appInfo, false);
|
||||||
|
deleteFile(cfgDir, "firefox-config.cfg"); // cleanup old version
|
||||||
|
File dest = new File(cfgDir.getPath(), CFG_FILE);
|
||||||
|
|
||||||
|
HashMap<String, String> fieldMap = new HashMap<>();
|
||||||
|
// Dynamic fields
|
||||||
|
fieldMap.put("%CERT_DATA%", certData);
|
||||||
|
fieldMap.put("%COMMON_NAME%", hostNames[0]);
|
||||||
|
fieldMap.put("%TIMESTAMP%", uninstall ? "-1" : "" + new Date().getTime());
|
||||||
|
fieldMap.put("%APP_PATH%", SystemUtilities.isMac() ? SystemUtilities.getAppPath() != null ? SystemUtilities.getAppPath().toString() : "" : "");
|
||||||
|
fieldMap.put("%UNINSTALL%", "" + uninstall);
|
||||||
|
|
||||||
|
FileUtilities.configureAssetFile(CFG_TEMPLATE, dest, fieldMap, LegacyFirefoxCertificateInstaller.class);
|
||||||
|
dest.setReadable(true, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
//
|
||||||
|
// Firefox AutoConfig Certificate Installer for Legacy Firefox versions
|
||||||
|
// This is part of the QZ Tray application
|
||||||
|
//
|
||||||
|
var serviceObserver = {
|
||||||
|
observe: function observe(aSubject, aTopic, aData) {
|
||||||
|
// Get NSS certdb object
|
||||||
|
var certdb = getCertDB();
|
||||||
|
|
||||||
|
if (needsUninstall()) {
|
||||||
|
deleteCertificate();
|
||||||
|
unregisterProtocol();
|
||||||
|
} else if (needsCert()) {
|
||||||
|
deleteCertificate();
|
||||||
|
installCertificate();
|
||||||
|
registerProtocol();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compares the timestamp embedded in this script against that stored in the browser's about:config
|
||||||
|
function needsCert() {
|
||||||
|
try {
|
||||||
|
return getPref("%PROPS_FILE%.installer.timestamp") != "%TIMESTAMP%";
|
||||||
|
} catch(notfound) {}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Installs the embedded base64 certificate into the browser
|
||||||
|
function installCertificate() {
|
||||||
|
certdb.addCertFromBase64(getCertData(), "C,C,C", "%COMMON_NAME% - %ABOUT_COMPANY%");
|
||||||
|
pref("%PROPS_FILE%.installer.timestamp", "%TIMESTAMP%");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deletes the certificate, if it exists
|
||||||
|
function deleteCertificate() {
|
||||||
|
var certs = certdb.getCerts();
|
||||||
|
var enumerator = certs.getEnumerator();
|
||||||
|
while (enumerator.hasMoreElements()) {
|
||||||
|
var cert = enumerator.getNext().QueryInterface(Components.interfaces.nsIX509Cert);
|
||||||
|
if (cert.containsEmailAddress("%ABOUT_EMAIL%")) {
|
||||||
|
try {
|
||||||
|
certdb.deleteCertificate(cert);
|
||||||
|
} catch (ignore) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pref("%PROPS_FILE%.installer.timestamp", "-1");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register the specified protocol to open with the specified application
|
||||||
|
function registerProtocol() {
|
||||||
|
// Only register if platform needs it (e.g. macOS)
|
||||||
|
var trayApp = "%APP_PATH%";
|
||||||
|
if (!trayApp) { return; }
|
||||||
|
try {
|
||||||
|
var hservice = Components.classes["@mozilla.org/uriloader/handler-service;1"].getService(Components.interfaces.nsIHandlerService);
|
||||||
|
var pservice = Components.classes["@mozilla.org/uriloader/external-protocol-service;1"].getService(Components.interfaces.nsIExternalProtocolService);
|
||||||
|
|
||||||
|
var file = Components.classes["@mozilla.org/file/local;1"].createInstance(Components.interfaces.nsIFile);
|
||||||
|
file.initWithPath(trayApp);
|
||||||
|
|
||||||
|
var lhandler = Components.classes["@mozilla.org/uriloader/local-handler-app;1"].createInstance(Components.interfaces.nsILocalHandlerApp);
|
||||||
|
lhandler.executable = file;
|
||||||
|
lhandler.name = "%PROPS_FILE%";
|
||||||
|
|
||||||
|
var protocol = pservice.getProtocolHandlerInfo("%DATA_DIR%");
|
||||||
|
protocol.preferredApplicationHandler = lhandler;
|
||||||
|
protocol.preferredAction = 2; // useHelperApp
|
||||||
|
protocol.alwaysAskBeforeHandling = false;
|
||||||
|
hservice.store(protocol);
|
||||||
|
} catch(ignore) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// De-register the specified protocol from opening with the specified application
|
||||||
|
function unregisterProtocol() {
|
||||||
|
// Only register if platform needs it (e.g. macOS)
|
||||||
|
var trayApp = "%APP_PATH%";
|
||||||
|
if (!trayApp) { return; }
|
||||||
|
try {
|
||||||
|
var hservice = Components.classes["@mozilla.org/uriloader/handler-service;1"].getService(Components.interfaces.nsIHandlerService);
|
||||||
|
var pservice = Components.classes["@mozilla.org/uriloader/external-protocol-service;1"].getService(Components.interfaces.nsIExternalProtocolService);
|
||||||
|
hservice.remove(pservice.getProtocolHandlerInfo("%DATA_DIR%"));
|
||||||
|
} catch(ignore) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get certdb object
|
||||||
|
function getCertDB() {
|
||||||
|
// Import certificate using NSS certdb API (http://tinyurl.com/x509certdb)
|
||||||
|
var id = "@mozilla.org/security/x509certdb;1";
|
||||||
|
var db1 = Components.classes[id].getService(Components.interfaces.nsIX509CertDB);
|
||||||
|
var db2 = db1;
|
||||||
|
try {
|
||||||
|
db2 = Components.classes[id].getService(Components.interfaces.nsIX509CertDB2);
|
||||||
|
} catch(ignore) {}
|
||||||
|
return db2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The certificate to import (automatically generated by desktop installer)
|
||||||
|
function getCertData() {
|
||||||
|
return "%CERT_DATA%";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Whether or not an uninstall should occur, flagged by the installer/uninstaller
|
||||||
|
function needsUninstall() {
|
||||||
|
try {
|
||||||
|
if (getPref("%PROPS_FILE%.installer.timestamp") == "-1") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(notfound) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return %UNINSTALL%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Components.utils.import("resource://gre/modules/Services.jsm");
|
||||||
|
Services.obs.addObserver(serviceObserver, "profile-after-change", false);
|
||||||
91
old code/tray/src/qz/installer/certificate/firefox/locator/AppAlias.java
Executable file
91
old code/tray/src/qz/installer/certificate/firefox/locator/AppAlias.java
Executable file
@@ -0,0 +1,91 @@
|
|||||||
|
package qz.installer.certificate.firefox.locator;
|
||||||
|
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
public enum AppAlias {
|
||||||
|
// Tor Browser intentionally excluded; Tor's proxy blocks localhost connections
|
||||||
|
FIREFOX(
|
||||||
|
new Alias("Mozilla", "Mozilla Firefox", "org.mozilla.firefox", true),
|
||||||
|
new Alias("Mozilla", "Firefox Developer Edition", "org.mozilla.firefoxdeveloperedition", true),
|
||||||
|
new Alias("Mozilla", "Firefox Nightly", "org.mozilla.nightly", true),
|
||||||
|
new Alias("Mozilla", "SeaMonkey", "org.mozilla.seamonkey", false),
|
||||||
|
new Alias("Waterfox", "Waterfox", "net.waterfox.waterfoxcurrent", true),
|
||||||
|
new Alias("Waterfox", "Waterfox Classic", "org.waterfoxproject.waterfox classic", false),
|
||||||
|
new Alias("Mozilla", "Pale Moon", "org.mozilla.palemoon", false),
|
||||||
|
// IceCat is technically enterprise ready, but not officially distributed for macOS, Windows
|
||||||
|
new Alias("Mozilla", "IceCat", "org.gnu.icecat", false)
|
||||||
|
);
|
||||||
|
Alias[] aliases;
|
||||||
|
AppAlias(Alias... aliases) {
|
||||||
|
this.aliases = aliases;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Alias[] getAliases() {
|
||||||
|
return aliases;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Alias findAlias(AppAlias appAlias, String appName, boolean stripVendor) {
|
||||||
|
if (appName != null) {
|
||||||
|
for (Alias alias : appAlias.aliases) {
|
||||||
|
if (appName.toLowerCase(Locale.ENGLISH).matches(alias.getName(stripVendor).toLowerCase(Locale.ENGLISH))) {
|
||||||
|
return alias;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Alias {
|
||||||
|
private String vendor;
|
||||||
|
private String name;
|
||||||
|
private String bundleId;
|
||||||
|
private boolean enterpriseReady;
|
||||||
|
private String posix;
|
||||||
|
|
||||||
|
public Alias(String vendor, String name, String bundleId, boolean enterpriseReady) {
|
||||||
|
this.name = name;
|
||||||
|
this.vendor = vendor;
|
||||||
|
this.bundleId = bundleId;
|
||||||
|
this.enterpriseReady = enterpriseReady;
|
||||||
|
this.posix = getName(true).replaceAll(" ", "").toLowerCase(Locale.ENGLISH);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getVendor() {
|
||||||
|
return vendor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove vendor prefix if exists
|
||||||
|
*/
|
||||||
|
public String getName(boolean stripVendor) {
|
||||||
|
if(stripVendor && "Mozilla".equals(vendor) && name.startsWith(vendor)) {
|
||||||
|
return name.substring(vendor.length()).trim();
|
||||||
|
}
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getBundleId() {
|
||||||
|
return bundleId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPosix() {
|
||||||
|
return posix;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether or not the app is known to recognizes enterprise policies, such as GPO
|
||||||
|
*/
|
||||||
|
public boolean isEnterpriseReady() {
|
||||||
|
return enterpriseReady;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
100
old code/tray/src/qz/installer/certificate/firefox/locator/AppInfo.java
Executable file
100
old code/tray/src/qz/installer/certificate/firefox/locator/AppInfo.java
Executable file
@@ -0,0 +1,100 @@
|
|||||||
|
package qz.installer.certificate.firefox.locator;
|
||||||
|
|
||||||
|
import com.github.zafarkhaja.semver.Version;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import qz.installer.certificate.firefox.locator.AppAlias.Alias;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Container class for installed app information
|
||||||
|
*/
|
||||||
|
public class AppInfo {
|
||||||
|
private AppAlias.Alias alias;
|
||||||
|
private Path path;
|
||||||
|
private Path exePath;
|
||||||
|
private Version version;
|
||||||
|
|
||||||
|
public AppInfo(Alias alias, Path exePath, String version) {
|
||||||
|
this.alias = alias;
|
||||||
|
this.path = exePath.getParent();
|
||||||
|
this.exePath = exePath;
|
||||||
|
this.version = parseVersion(version);
|
||||||
|
}
|
||||||
|
|
||||||
|
public AppInfo(Alias alias, Path path, Path exePath, String version) {
|
||||||
|
this.alias = alias;
|
||||||
|
this.path = path;
|
||||||
|
this.exePath = exePath;
|
||||||
|
this.version = parseVersion(version);
|
||||||
|
}
|
||||||
|
|
||||||
|
public AppInfo(Alias alias, Path exePath) {
|
||||||
|
this.alias = alias;
|
||||||
|
this.path = exePath.getParent();
|
||||||
|
this.exePath = exePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Alias getAlias() {
|
||||||
|
return alias;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName(boolean stripVendor) {
|
||||||
|
return alias.getName(stripVendor);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Path getExePath() {
|
||||||
|
return exePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Path getPath() {
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPath(Path path) {
|
||||||
|
this.path = path;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Version getVersion() {
|
||||||
|
return version;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setVersion(String version) {
|
||||||
|
this.version = parseVersion(version);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setVersion(Version version) {
|
||||||
|
this.version = version;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Version parseVersion(String version) {
|
||||||
|
try {
|
||||||
|
// Ensure < 3 octets (e.g. "56.0") doesn't failing
|
||||||
|
while(version.split("\\.").length < 3) {
|
||||||
|
version = version + ".0";
|
||||||
|
}
|
||||||
|
return Version.valueOf(version);
|
||||||
|
} catch(Exception ignore1) {
|
||||||
|
// Catch poor formatting (e.g. "97.0a1"), try to use major version only
|
||||||
|
if(version.split("\\.").length > 0) {
|
||||||
|
try {
|
||||||
|
String[] tryFix = version.split("\\.");
|
||||||
|
return Version.valueOf(tryFix[0] + ".0.0-unknown");
|
||||||
|
} catch(Exception ignore2) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if(o instanceof AppInfo && o != null && path != null) {
|
||||||
|
return path.equals(((AppInfo)o).getPath());
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return alias + " " + path;
|
||||||
|
}
|
||||||
|
}
|
||||||
87
old code/tray/src/qz/installer/certificate/firefox/locator/AppLocator.java
Executable file
87
old code/tray/src/qz/installer/certificate/firefox/locator/AppLocator.java
Executable file
@@ -0,0 +1,87 @@
|
|||||||
|
package qz.installer.certificate.firefox.locator;
|
||||||
|
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
import qz.utils.ShellUtilities;
|
||||||
|
import qz.utils.SystemUtilities;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
|
||||||
|
public abstract class AppLocator {
|
||||||
|
protected static final Logger log = LogManager.getLogger(AppLocator.class);
|
||||||
|
|
||||||
|
private static AppLocator INSTANCE = getPlatformSpecificAppLocator();
|
||||||
|
|
||||||
|
public abstract ArrayList<AppInfo> locate(AppAlias appAlias);
|
||||||
|
public abstract ArrayList<Path> getPidPaths(ArrayList<String> pids);
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
public ArrayList<String> getPids(String ... processNames) {
|
||||||
|
return getPids(new ArrayList<>(Arrays.asList(processNames)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Linux, Mac
|
||||||
|
*/
|
||||||
|
public ArrayList<String> getPids(ArrayList<String> processNames) {
|
||||||
|
String[] response;
|
||||||
|
ArrayList<String> pidList = new ArrayList<>();
|
||||||
|
|
||||||
|
if(processNames.contains("firefox") && !(SystemUtilities.isWindows() || SystemUtilities.isMac())) {
|
||||||
|
processNames.add("MainThread"); // Workaround Firefox 79 https://github.com/qzind/tray/issues/701
|
||||||
|
processNames.add("GeckoMain"); // Workaround Firefox 94 https://bugzilla.mozilla.org/show_bug.cgi?id=1742606
|
||||||
|
}
|
||||||
|
|
||||||
|
if (processNames.size() == 0) return pidList;
|
||||||
|
|
||||||
|
// Quoting handled by the command processor (e.g. pgrep -x "myapp|my app" is perfectly valid)
|
||||||
|
String data = ShellUtilities.executeRaw("pgrep", "-x", String.join("|", processNames));
|
||||||
|
|
||||||
|
//Splitting an empty string results in a 1 element array, this is not what we want
|
||||||
|
if (!data.isEmpty()) {
|
||||||
|
response = data.split("\\s*\\r?\\n");
|
||||||
|
Collections.addAll(pidList, response);
|
||||||
|
}
|
||||||
|
|
||||||
|
return pidList;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ArrayList<Path> getRunningPaths(ArrayList<AppInfo> appList) {
|
||||||
|
return getRunningPaths(appList, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the path to the running executables matching on <code>AppInfo.getExePath</code>
|
||||||
|
* This is resource intensive; if a non-null <code>cache</code> is provided, it will return that instead
|
||||||
|
*/
|
||||||
|
public static ArrayList<Path> getRunningPaths(ArrayList<AppInfo> appList, ArrayList<Path> cache) {
|
||||||
|
if(cache == null) {
|
||||||
|
ArrayList<String> appNames = new ArrayList<>();
|
||||||
|
for(AppInfo app : appList) {
|
||||||
|
String exeName = app.getExePath().getFileName().toString();
|
||||||
|
if (!appNames.contains(exeName)) appNames.add(exeName);
|
||||||
|
}
|
||||||
|
cache = INSTANCE.getPidPaths(INSTANCE.getPids(appNames));
|
||||||
|
}
|
||||||
|
|
||||||
|
return cache;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static AppLocator getInstance() {
|
||||||
|
return INSTANCE;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static AppLocator getPlatformSpecificAppLocator() {
|
||||||
|
switch(SystemUtilities.getOs()) {
|
||||||
|
case WINDOWS:
|
||||||
|
return new WindowsAppLocator();
|
||||||
|
case MAC:
|
||||||
|
return new MacAppLocator();
|
||||||
|
default:
|
||||||
|
return new LinuxAppLocator();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
159
old code/tray/src/qz/installer/certificate/firefox/locator/LinuxAppLocator.java
Executable file
159
old code/tray/src/qz/installer/certificate/firefox/locator/LinuxAppLocator.java
Executable file
@@ -0,0 +1,159 @@
|
|||||||
|
package qz.installer.certificate.firefox.locator;
|
||||||
|
|
||||||
|
import org.apache.commons.io.FilenameUtils;
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
import qz.utils.ShellUtilities;
|
||||||
|
import qz.utils.SystemUtilities;
|
||||||
|
import qz.utils.UnixUtilities;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
|
||||||
|
public class LinuxAppLocator extends AppLocator {
|
||||||
|
private static final Logger log = LogManager.getLogger(LinuxAppLocator.class);
|
||||||
|
|
||||||
|
public ArrayList<AppInfo> locate(AppAlias appAlias) {
|
||||||
|
ArrayList<AppInfo> appList = new ArrayList<>();
|
||||||
|
|
||||||
|
// Workaround for calling "firefox --version" as sudo
|
||||||
|
String[] env = appendPaths("HOME=/tmp");
|
||||||
|
|
||||||
|
// Search for matching executable in all path values
|
||||||
|
aliasLoop:
|
||||||
|
for(AppAlias.Alias alias : appAlias.aliases) {
|
||||||
|
// Add non-standard app search locations (e.g. Fedora)
|
||||||
|
for (String dirname : appendPaths(alias.getPosix(), "/usr/lib/$/bin", "/usr/lib64/$/bin", "/usr/lib/$", "/usr/lib64/$")) {
|
||||||
|
Path path = Paths.get(dirname, alias.getPosix());
|
||||||
|
if (Files.isRegularFile(path) && Files.isExecutable(path)) {
|
||||||
|
log.info("Found {} {}: {}, investigating...", alias.getVendor(), alias.getName(true), path);
|
||||||
|
try {
|
||||||
|
File file = path.toFile().getCanonicalFile(); // fix symlinks
|
||||||
|
if(file.getPath().endsWith("/snap")) {
|
||||||
|
// Ubuntu 22.04+ ships Firefox as a snap
|
||||||
|
// Snaps are read-only and are symlinks back to /usr/bin/snap
|
||||||
|
// Reset the executable back to /snap/bin/firefox to get proper version information
|
||||||
|
file = path.toFile();
|
||||||
|
}
|
||||||
|
if(file.getPath().endsWith(".sh")) {
|
||||||
|
// Legacy Ubuntu likes to use .../firefox/firefox.sh, return .../firefox/firefox instead
|
||||||
|
log.info("Found an '.sh' file: {}, removing file extension: {}", file, file = new File(FilenameUtils.removeExtension(file.getPath())));
|
||||||
|
}
|
||||||
|
String contentType = Files.probeContentType(file.toPath());
|
||||||
|
if(contentType == null) {
|
||||||
|
// Fallback to commandline per https://bugs.openjdk.org/browse/JDK-8188228
|
||||||
|
contentType = ShellUtilities.executeRaw("file", "--mime-type", "--brief", file.getPath()).trim();
|
||||||
|
}
|
||||||
|
if(contentType != null && contentType.endsWith("/x-shellscript")) {
|
||||||
|
if(UnixUtilities.isFedora()) {
|
||||||
|
// Firefox's script is full of variables and not parsable, fallback to /usr/lib64/$, etc
|
||||||
|
log.info("Found shell script at {}, but we're on Fedora, so we'll look in some known locations instead.", file.getPath());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Debian and Arch like to place a stub script directly in /usr/bin/
|
||||||
|
// TODO: Split into a function; possibly recurse on search paths
|
||||||
|
log.info("{} bin was expected but script found... Reading...", appAlias.name());
|
||||||
|
BufferedReader reader = new BufferedReader(new FileReader(file));
|
||||||
|
String line;
|
||||||
|
while((line = reader.readLine()) != null) {
|
||||||
|
if(line.startsWith("exec") && line.contains(alias.getPosix())) {
|
||||||
|
String[] parts = line.split(" ");
|
||||||
|
// Get the app name after "exec"
|
||||||
|
if (parts.length > 1) {
|
||||||
|
log.info("Found a familiar line '{}', using '{}'", line, parts[1]);
|
||||||
|
Path p = Paths.get(parts[1]);
|
||||||
|
String exec = parts[1];
|
||||||
|
// Handle edge-case for esr release
|
||||||
|
if(!p.isAbsolute()) {
|
||||||
|
// Script doesn't contain the full path, go deeper
|
||||||
|
exec = Paths.get(dirname, exec).toFile().getCanonicalPath();
|
||||||
|
log.info("Calculated full bin path {}", exec);
|
||||||
|
}
|
||||||
|
// Make sure it actually exists
|
||||||
|
if(!(file = new File(exec)).exists()) {
|
||||||
|
log.warn("Sorry, we couldn't detect the real path of {}. Skipping...", appAlias.name());
|
||||||
|
continue aliasLoop;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reader.close();
|
||||||
|
} else {
|
||||||
|
log.info("Assuming {} {} is installed: {}", alias.getVendor(), alias.getName(true), file);
|
||||||
|
}
|
||||||
|
AppInfo appInfo = new AppInfo(alias, file.toPath());
|
||||||
|
if(file.getPath().startsWith("/snap/")) {
|
||||||
|
// Ubuntu 22.04+ uses snaps, fallback to a sane "path" value
|
||||||
|
String snapPath = file.getPath(); // e.g. /snap/bin/firefox
|
||||||
|
snapPath = snapPath.replaceFirst("/bin/", "/");
|
||||||
|
snapPath += "/current";
|
||||||
|
appInfo.setPath(Paths.get(snapPath));
|
||||||
|
}
|
||||||
|
|
||||||
|
appList.add(appInfo);
|
||||||
|
|
||||||
|
// Call "--version" on executable to obtain version information
|
||||||
|
Process p = Runtime.getRuntime().exec(new String[] {file.getPath(), "--version" }, env);
|
||||||
|
BufferedReader reader = new BufferedReader(new InputStreamReader(p.getInputStream()));
|
||||||
|
String version = reader.readLine();
|
||||||
|
reader.close();
|
||||||
|
if (version != null) {
|
||||||
|
log.info("We obtained version info: {}, but we'll need to parse it", version);
|
||||||
|
if(version.contains(" ")) {
|
||||||
|
String[] split = version.split(" ");
|
||||||
|
String parsed = split[split.length - 1];
|
||||||
|
String stripped = parsed.replaceAll("[^\\d.]", "");
|
||||||
|
appInfo.setVersion(stripped);
|
||||||
|
if(!parsed.equals(stripped)) {
|
||||||
|
// Add the meta data back (e.g. "esr")
|
||||||
|
appInfo.getVersion().setBuildMetadata(parsed.replaceAll("[\\d.]", ""));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
appInfo.setVersion(version.trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
} catch(Exception e) {
|
||||||
|
log.warn("Something went wrong getting app info for {} {}", alias.getVendor(), alias.getName(true), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return appList;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ArrayList<Path> getPidPaths(ArrayList<String> pids) {
|
||||||
|
ArrayList<Path> pathList = new ArrayList<>();
|
||||||
|
|
||||||
|
for(String pid : pids) {
|
||||||
|
try {
|
||||||
|
pathList.add(Paths.get("/proc/", pid, !SystemUtilities.isSolaris() ? "/exe" : "/path/a.out").toRealPath());
|
||||||
|
} catch(IOException e) {
|
||||||
|
log.warn("Process {} vanished", pid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return pathList;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a PATH value with provided paths appended, replacing "$" with POSIX app name
|
||||||
|
* Useful for strange Firefox install locations (e.g. Fedora)
|
||||||
|
*
|
||||||
|
* Usage: appendPaths("firefox", "/usr/lib64");
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
private static String[] appendPaths(String posix, String ... prefixes) {
|
||||||
|
String newPath = System.getenv("PATH");
|
||||||
|
for (String prefix : prefixes) {
|
||||||
|
newPath = newPath + File.pathSeparator + prefix.replaceAll("\\$", posix);
|
||||||
|
}
|
||||||
|
return newPath.split(File.pathSeparator);
|
||||||
|
}
|
||||||
|
}
|
||||||
168
old code/tray/src/qz/installer/certificate/firefox/locator/MacAppLocator.java
Executable file
168
old code/tray/src/qz/installer/certificate/firefox/locator/MacAppLocator.java
Executable file
@@ -0,0 +1,168 @@
|
|||||||
|
package qz.installer.certificate.firefox.locator;
|
||||||
|
|
||||||
|
import com.sun.jna.Library;
|
||||||
|
import com.sun.jna.Memory;
|
||||||
|
import com.sun.jna.Native;
|
||||||
|
import com.sun.jna.Pointer;
|
||||||
|
import com.sun.jna.ptr.IntByReference;
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
import org.w3c.dom.Document;
|
||||||
|
import org.w3c.dom.Node;
|
||||||
|
import org.w3c.dom.NodeList;
|
||||||
|
import org.xml.sax.SAXException;
|
||||||
|
import qz.utils.ShellUtilities;
|
||||||
|
|
||||||
|
import javax.xml.parsers.DocumentBuilderFactory;
|
||||||
|
import javax.xml.parsers.ParserConfigurationException;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Iterator;
|
||||||
|
|
||||||
|
public class MacAppLocator extends AppLocator{
|
||||||
|
protected static final Logger log = LogManager.getLogger(MacAppLocator.class);
|
||||||
|
|
||||||
|
private static String[] BLACKLISTED_PATHS = new String[]{"/Volumes/", "/.Trash/", "/Applications (Parallels)/" };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper class for finding key/value siblings from the DDM
|
||||||
|
*/
|
||||||
|
private enum SiblingNode {
|
||||||
|
NAME("_name"),
|
||||||
|
PATH("path"),
|
||||||
|
VERSION("version");
|
||||||
|
|
||||||
|
private String key;
|
||||||
|
private boolean wants;
|
||||||
|
|
||||||
|
SiblingNode(String key) {
|
||||||
|
this.key = key;
|
||||||
|
this.wants = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isKey(Node node) {
|
||||||
|
if (node.getNodeName().equals("key") && node.getTextContent().equals(key)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ArrayList<AppInfo> locate(AppAlias appAlias) {
|
||||||
|
ArrayList<AppInfo> appList = new ArrayList<>();
|
||||||
|
Document doc;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// system_profile benchmarks about 30% better than lsregister
|
||||||
|
Process p = Runtime.getRuntime().exec(new String[] {"system_profiler", "SPApplicationsDataType", "-xml"}, ShellUtilities.envp);
|
||||||
|
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
|
||||||
|
// don't let the <!DOCTYPE> fail parsing per https://github.com/qzind/tray/issues/809
|
||||||
|
dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
|
||||||
|
doc = dbf.newDocumentBuilder().parse(p.getInputStream());
|
||||||
|
} catch(IOException | ParserConfigurationException | SAXException e) {
|
||||||
|
log.warn("Could not retrieve app listing for {}", appAlias.name(), e);
|
||||||
|
return appList;
|
||||||
|
}
|
||||||
|
doc.normalizeDocument();
|
||||||
|
|
||||||
|
NodeList nodeList = doc.getElementsByTagName("dict");
|
||||||
|
for (int i = 0; i < nodeList.getLength(); i++) {
|
||||||
|
NodeList dict = nodeList.item(i).getChildNodes();
|
||||||
|
HashMap<SiblingNode, String> foundApp = new HashMap<>();
|
||||||
|
for (int j = 0; j < dict.getLength(); j++) {
|
||||||
|
Node node = dict.item(j);
|
||||||
|
if (node.getNodeType() == Node.ELEMENT_NODE) {
|
||||||
|
for (SiblingNode sibling : SiblingNode.values()) {
|
||||||
|
if (sibling.wants) {
|
||||||
|
foundApp.put(sibling, node.getTextContent());
|
||||||
|
sibling.wants = false;
|
||||||
|
break;
|
||||||
|
} else if(sibling.isKey(node)) {
|
||||||
|
sibling.wants = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AppAlias.Alias alias;
|
||||||
|
if((alias = AppAlias.findAlias(appAlias, foundApp.get(SiblingNode.NAME), true)) != null) {
|
||||||
|
appList.add(new AppInfo(alias, Paths.get(foundApp.get(SiblingNode.PATH)),
|
||||||
|
getExePath(foundApp.get(SiblingNode.PATH)), foundApp.get(SiblingNode.VERSION)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove blacklisted paths
|
||||||
|
Iterator<AppInfo> appInfoIterator = appList.iterator();
|
||||||
|
while(appInfoIterator.hasNext()) {
|
||||||
|
AppInfo appInfo = appInfoIterator.next();
|
||||||
|
for(String listEntry : BLACKLISTED_PATHS) {
|
||||||
|
if (appInfo.getPath() != null && appInfo.getPath().toString().contains(listEntry)) {
|
||||||
|
appInfoIterator.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return appList;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ArrayList<Path> getPidPaths(ArrayList<String> pids) {
|
||||||
|
ArrayList<Path> processPaths = new ArrayList();
|
||||||
|
for (String pid : pids) {
|
||||||
|
Pointer buf = new Memory(SystemB.PROC_PIDPATHINFO_MAXSIZE);
|
||||||
|
SystemB.INSTANCE.proc_pidpath(Integer.parseInt(pid), buf, SystemB.PROC_PIDPATHINFO_MAXSIZE);
|
||||||
|
processPaths.add(Paths.get(buf.getString(0).trim()));
|
||||||
|
}
|
||||||
|
return processPaths;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate executable path by parsing Contents/Info.plist
|
||||||
|
*/
|
||||||
|
private static Path getExePath(String appPath) {
|
||||||
|
Path path = Paths.get(appPath).toAbsolutePath().normalize();
|
||||||
|
Path plist = path.resolve("Contents/Info.plist");
|
||||||
|
Document doc;
|
||||||
|
try {
|
||||||
|
if(!plist.toFile().exists()) {
|
||||||
|
log.warn("Could not locate plist file for {}: {}", appPath, plist);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// Convert potentially binary plist files to XML
|
||||||
|
Process p = Runtime.getRuntime().exec(new String[] {"plutil", "-convert", "xml1", plist.toString(), "-o", "-"}, ShellUtilities.envp);
|
||||||
|
doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(p.getInputStream());
|
||||||
|
} catch(IOException | ParserConfigurationException | SAXException e) {
|
||||||
|
log.warn("Could not parse plist file for {}: {}", appPath, appPath, e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
doc.normalizeDocument();
|
||||||
|
|
||||||
|
boolean upNext = false;
|
||||||
|
NodeList nodeList = doc.getElementsByTagName("dict");
|
||||||
|
for (int i = 0; i < nodeList.getLength(); i++) {
|
||||||
|
NodeList dict = nodeList.item(i).getChildNodes();
|
||||||
|
for(int j = 0; j < dict.getLength(); j++) {
|
||||||
|
Node node = dict.item(j);
|
||||||
|
if ("key".equals(node.getNodeName()) && node.getTextContent().equals("CFBundleExecutable")) {
|
||||||
|
upNext = true;
|
||||||
|
} else if (upNext && "string".equals(node.getNodeName())) {
|
||||||
|
return path.resolve("Contents/MacOS/" + node.getTextContent());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private interface SystemB extends Library {
|
||||||
|
SystemB INSTANCE = Native.load("System", SystemB.class);
|
||||||
|
int PROC_ALL_PIDS = 1;
|
||||||
|
int PROC_PIDPATHINFO_MAXSIZE = 1024 * 4;
|
||||||
|
int sysctlbyname(String name, Pointer oldp, IntByReference oldlenp, Pointer newp, int newlen);
|
||||||
|
int proc_listpids(int type, int typeinfo, int[] buffer, int buffersize);
|
||||||
|
int proc_pidpath(int pid, Pointer buffer, int buffersize);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
/**
|
||||||
|
* @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 qz.installer.certificate.firefox.locator;
|
||||||
|
|
||||||
|
import com.sun.jna.Memory;
|
||||||
|
import com.sun.jna.Native;
|
||||||
|
import com.sun.jna.Pointer;
|
||||||
|
import com.sun.jna.platform.win32.Kernel32;
|
||||||
|
import com.sun.jna.platform.win32.Psapi;
|
||||||
|
import com.sun.jna.platform.win32.Tlhelp32;
|
||||||
|
import com.sun.jna.platform.win32.WinNT;
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
import qz.installer.certificate.firefox.locator.AppAlias.Alias;
|
||||||
|
import qz.utils.WindowsUtilities;
|
||||||
|
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
import static com.sun.jna.platform.win32.WinReg.HKEY_LOCAL_MACHINE;
|
||||||
|
|
||||||
|
public class WindowsAppLocator extends AppLocator{
|
||||||
|
protected static final Logger log = LogManager.getLogger(MacAppLocator.class);
|
||||||
|
|
||||||
|
private static String REG_TEMPLATE = "Software\\%s%s\\%s%s";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ArrayList<AppInfo> locate(AppAlias appAlias) {
|
||||||
|
ArrayList<AppInfo> appList = new ArrayList<>();
|
||||||
|
for (Alias alias : appAlias.aliases) {
|
||||||
|
if (alias.getVendor() != null) {
|
||||||
|
String[] suffixes = new String[]{ "", " ESR"};
|
||||||
|
String[] prefixes = new String[]{ "", "WOW6432Node\\"};
|
||||||
|
for (String suffix : suffixes) {
|
||||||
|
for (String prefix : prefixes) {
|
||||||
|
String key = String.format(REG_TEMPLATE, prefix, alias.getVendor(), alias.getName(), suffix);
|
||||||
|
AppInfo appInfo = getAppInfo(alias, key, suffix);
|
||||||
|
if (appInfo != null && !appList.contains(appInfo)) {
|
||||||
|
appList.add(appInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return appList;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ArrayList<String> getPids(ArrayList<String> processNames) {
|
||||||
|
ArrayList<String> pidList = new ArrayList<>();
|
||||||
|
|
||||||
|
if (processNames.isEmpty()) return pidList;
|
||||||
|
|
||||||
|
Tlhelp32.PROCESSENTRY32 pe32 = new Tlhelp32.PROCESSENTRY32();
|
||||||
|
pe32.dwSize = new WinNT.DWORD(pe32.size());
|
||||||
|
|
||||||
|
// Fetch a snapshot of all processes
|
||||||
|
WinNT.HANDLE hSnapshot = Kernel32.INSTANCE.CreateToolhelp32Snapshot(Tlhelp32.TH32CS_SNAPPROCESS, new WinNT.DWORD(0));
|
||||||
|
if (hSnapshot.equals(WinNT.INVALID_HANDLE_VALUE)) {
|
||||||
|
log.warn("Process snapshot has invalid handle");
|
||||||
|
return pidList;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Kernel32.INSTANCE.Process32First(hSnapshot, pe32)) {
|
||||||
|
do {
|
||||||
|
String processName = Native.toString(pe32.szExeFile);
|
||||||
|
if(processNames.contains(processName.toLowerCase(Locale.ENGLISH))) {
|
||||||
|
pidList.add(pe32.th32ProcessID.toString());
|
||||||
|
}
|
||||||
|
} while (Kernel32.INSTANCE.Process32Next(hSnapshot, pe32));
|
||||||
|
}
|
||||||
|
|
||||||
|
Kernel32.INSTANCE.CloseHandle(hSnapshot);
|
||||||
|
return pidList;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ArrayList<Path> getPidPaths(ArrayList<String> pids) {
|
||||||
|
ArrayList<Path> pathList = new ArrayList<>();
|
||||||
|
|
||||||
|
for(String pid : pids) {
|
||||||
|
WinNT.HANDLE hProcess = Kernel32.INSTANCE.OpenProcess(WinNT.PROCESS_QUERY_INFORMATION | WinNT.PROCESS_VM_READ, false, Integer.parseInt(pid));
|
||||||
|
if (hProcess == null) {
|
||||||
|
log.warn("Handle for PID {} is missing, skipping.", pid);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
int bufferSize = WinNT.MAX_PATH;
|
||||||
|
Pointer buffer = new Memory(bufferSize * Native.WCHAR_SIZE);
|
||||||
|
|
||||||
|
if (Psapi.INSTANCE.GetModuleFileNameEx(hProcess, null, buffer, bufferSize) == 0) {
|
||||||
|
log.warn("Full path to PID {} is empty, skipping.", pid);
|
||||||
|
Kernel32.INSTANCE.CloseHandle(hProcess);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Kernel32.INSTANCE.CloseHandle(hProcess);
|
||||||
|
pathList.add(Paths.get(Native.WCHAR_SIZE == 1 ?
|
||||||
|
buffer.getString(0) :
|
||||||
|
buffer.getWideString(0)));
|
||||||
|
}
|
||||||
|
return pathList;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use a proprietary Firefox-only technique for getting "PathToExe" registry value
|
||||||
|
*/
|
||||||
|
private static AppInfo getAppInfo(Alias alias, String key, String suffix) {
|
||||||
|
String version = WindowsUtilities.getRegString(HKEY_LOCAL_MACHINE, key, "CurrentVersion");
|
||||||
|
if (version != null) {
|
||||||
|
version = version.split(" ")[0]; // chop off (x86 ...)
|
||||||
|
if (!suffix.isEmpty()) {
|
||||||
|
if (key.endsWith(suffix)) {
|
||||||
|
key = key.substring(0, key.length() - suffix.length());
|
||||||
|
}
|
||||||
|
version = version + suffix;
|
||||||
|
}
|
||||||
|
String exePath = WindowsUtilities.getRegString(HKEY_LOCAL_MACHINE, key + " " + version + "\\bin", "PathToExe");
|
||||||
|
|
||||||
|
if (exePath != null) {
|
||||||
|
// SemVer: Replace spaces in suffixes with dashes
|
||||||
|
version = version.replaceAll(" ", "-");
|
||||||
|
return new AppInfo(alias, Paths.get(exePath), version);
|
||||||
|
} else {
|
||||||
|
log.warn("Couldn't locate \"PathToExe\" for \"{}\" in \"{}\", skipping", alias.getName(), key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
161
old code/tray/src/qz/installer/provision/ProvisionInstaller.java
Executable file
161
old code/tray/src/qz/installer/provision/ProvisionInstaller.java
Executable file
@@ -0,0 +1,161 @@
|
|||||||
|
package qz.installer.provision;
|
||||||
|
|
||||||
|
import org.apache.commons.io.FileUtils;
|
||||||
|
import org.apache.commons.io.IOUtils;
|
||||||
|
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.Step;
|
||||||
|
import qz.build.provision.params.Os;
|
||||||
|
import qz.build.provision.params.Phase;
|
||||||
|
import qz.build.provision.params.types.Script;
|
||||||
|
import qz.build.provision.params.types.Software;
|
||||||
|
import qz.common.Constants;
|
||||||
|
import qz.installer.provision.invoker.*;
|
||||||
|
import qz.utils.ShellUtilities;
|
||||||
|
import qz.utils.SystemUtilities;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
|
||||||
|
import static qz.common.Constants.*;
|
||||||
|
import static qz.utils.FileUtilities.*;
|
||||||
|
|
||||||
|
public class ProvisionInstaller {
|
||||||
|
protected static final Logger log = LogManager.getLogger(ProvisionInstaller.class);
|
||||||
|
private ArrayList<Step> steps;
|
||||||
|
|
||||||
|
static {
|
||||||
|
// Populate variables for scripting environment
|
||||||
|
ShellUtilities.addEnvp("APP_TITLE", ABOUT_TITLE,
|
||||||
|
"APP_VERSION", VERSION,
|
||||||
|
"APP_ABBREV", PROPS_FILE,
|
||||||
|
"APP_VENDOR", ABOUT_COMPANY,
|
||||||
|
"APP_VENDOR_ABBREV", DATA_DIR,
|
||||||
|
"APP_ARCH", SystemUtilities.getArch(),
|
||||||
|
"APP_OS", SystemUtilities.getOs(),
|
||||||
|
"APP_DIR", SystemUtilities.getAppPath(),
|
||||||
|
"APP_USER_DIR", USER_DIR,
|
||||||
|
"APP_SHARED_DIR", SHARED_DIR);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ProvisionInstaller(Path relativePath) throws IOException, JSONException {
|
||||||
|
this(relativePath, relativePath.resolve(Constants.PROVISION_FILE).toFile());
|
||||||
|
}
|
||||||
|
|
||||||
|
public ProvisionInstaller(Path relativePath, File jsonFile) throws IOException, JSONException {
|
||||||
|
if(!jsonFile.exists()) {
|
||||||
|
log.info("Provision file not found '{}', skipping", jsonFile);
|
||||||
|
this.steps = new ArrayList<>();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.steps = parse(FileUtils.readFileToString(jsonFile, StandardCharsets.UTF_8), relativePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Package private for internal testing only
|
||||||
|
* Assumes files located in ./resources/ subdirectory
|
||||||
|
*/
|
||||||
|
ProvisionInstaller(Class relativeClass, InputStream in) throws IOException, JSONException {
|
||||||
|
this(relativeClass, IOUtils.toString(in, StandardCharsets.UTF_8));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Package private for internal testing only
|
||||||
|
* Assumes files located in ./resources/ subdirectory
|
||||||
|
*/
|
||||||
|
ProvisionInstaller(Class relativeClass, String jsonData) throws JSONException {
|
||||||
|
this.steps = parse(jsonData, relativeClass);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void invoke(Phase phase) {
|
||||||
|
for(Step step : this.steps) {
|
||||||
|
if(phase == null || step.getPhase() == phase) {
|
||||||
|
try {
|
||||||
|
invokeStep(step);
|
||||||
|
}
|
||||||
|
catch(Exception e) {
|
||||||
|
log.error("[PROVISION] Provisioning step failed '{}'", step, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void invoke() {
|
||||||
|
invoke(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ArrayList<Step> parse(String jsonData, Object relativeObject) throws JSONException {
|
||||||
|
return parse(new JSONArray(jsonData), relativeObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean invokeStep(Step step) throws Exception {
|
||||||
|
if(Os.matchesHost(step.getOs())) {
|
||||||
|
log.info("[PROVISION] Invoking step '{}'", step.toString());
|
||||||
|
} else {
|
||||||
|
log.info("[PROVISION] Skipping step for different OS '{}'", step.toString());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Invokable invoker;
|
||||||
|
switch(step.getType()) {
|
||||||
|
case CA:
|
||||||
|
invoker = new CaInvoker(step, PropertyInvoker.getProperties(step));
|
||||||
|
break;
|
||||||
|
case CERT:
|
||||||
|
invoker = new CertInvoker(step);
|
||||||
|
break;
|
||||||
|
case CONF:
|
||||||
|
invoker = new ConfInvoker(step);
|
||||||
|
break;
|
||||||
|
case SCRIPT:
|
||||||
|
invoker = new ScriptInvoker(step);
|
||||||
|
break;
|
||||||
|
case SOFTWARE:
|
||||||
|
invoker = new SoftwareInvoker(step);
|
||||||
|
break;
|
||||||
|
case REMOVER:
|
||||||
|
invoker = new RemoverInvoker(step);
|
||||||
|
break;
|
||||||
|
case RESOURCE:
|
||||||
|
invoker = new ResourceInvoker(step);
|
||||||
|
break;
|
||||||
|
case PREFERENCE:
|
||||||
|
invoker = new PropertyInvoker(step, PropertyInvoker.getPreferences(step));
|
||||||
|
break;
|
||||||
|
case PROPERTY:
|
||||||
|
invoker = new PropertyInvoker(step, PropertyInvoker.getProperties(step));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new UnsupportedOperationException("Type " + step.getType() + " is not yet supported.");
|
||||||
|
}
|
||||||
|
return invoker.invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ArrayList<Step> getSteps() {
|
||||||
|
return steps;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ArrayList<Step> parse(JSONArray jsonArray, Object relativeObject) throws JSONException {
|
||||||
|
ArrayList<Step> steps = new ArrayList<>();
|
||||||
|
for(int i = 0; i < jsonArray.length(); i++) {
|
||||||
|
JSONObject jsonStep = jsonArray.getJSONObject(i);
|
||||||
|
try {
|
||||||
|
steps.add(Step.parse(jsonStep, relativeObject));
|
||||||
|
} catch(Exception e) {
|
||||||
|
log.warn("[PROVISION] Unable to add step '{}'", jsonStep, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return steps;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean shouldBeExecutable(Path path) {
|
||||||
|
return Script.parse(path) != null || Software.parse(path) != Software.UNKNOWN;
|
||||||
|
}
|
||||||
|
}
|
||||||
49
old code/tray/src/qz/installer/provision/invoker/CaInvoker.java
Executable file
49
old code/tray/src/qz/installer/provision/invoker/CaInvoker.java
Executable file
@@ -0,0 +1,49 @@
|
|||||||
|
package qz.installer.provision.invoker;
|
||||||
|
|
||||||
|
import qz.build.provision.Step;
|
||||||
|
import qz.common.PropertyHelper;
|
||||||
|
import qz.utils.ArgValue;
|
||||||
|
import qz.utils.FileUtilities;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Combines ResourceInvoker and PropertyInvoker to deploy a file and set a property to its deployed path
|
||||||
|
*/
|
||||||
|
public class CaInvoker extends InvokableResource {
|
||||||
|
Step step;
|
||||||
|
PropertyHelper properties;
|
||||||
|
|
||||||
|
public CaInvoker(Step step, PropertyHelper properties) {
|
||||||
|
this.step = step;
|
||||||
|
this.properties = properties;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean invoke() throws IOException {
|
||||||
|
// First, write our cert file
|
||||||
|
File caCert = dataToFile(step);
|
||||||
|
if(caCert == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next, handle our property step
|
||||||
|
Step propsStep = step.clone();
|
||||||
|
|
||||||
|
// If the property already exists, snag it
|
||||||
|
String key = ArgValue.AUTHCERT_OVERRIDE.getMatch();
|
||||||
|
String value = caCert.getPath();
|
||||||
|
if (properties.containsKey(key)) {
|
||||||
|
value = properties.getProperty(key) + FileUtilities.FILE_SEPARATOR + value;
|
||||||
|
}
|
||||||
|
|
||||||
|
propsStep.setData(String.format("%s=%s", key, value));
|
||||||
|
|
||||||
|
if (new PropertyInvoker(propsStep, properties).invoke()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
26
old code/tray/src/qz/installer/provision/invoker/CertInvoker.java
Executable file
26
old code/tray/src/qz/installer/provision/invoker/CertInvoker.java
Executable file
@@ -0,0 +1,26 @@
|
|||||||
|
package qz.installer.provision.invoker;
|
||||||
|
|
||||||
|
import qz.build.provision.Step;
|
||||||
|
import qz.common.Constants;
|
||||||
|
import qz.utils.FileUtilities;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
|
||||||
|
import static qz.utils.ArgParser.ExitStatus.*;
|
||||||
|
|
||||||
|
public class CertInvoker extends InvokableResource {
|
||||||
|
private Step step;
|
||||||
|
|
||||||
|
public CertInvoker(Step step) {
|
||||||
|
this.step = step;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean invoke() throws Exception {
|
||||||
|
File cert = dataToFile(step);
|
||||||
|
if(cert == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return FileUtilities.addToCertList(Constants.ALLOW_FILE, cert) == SUCCESS;
|
||||||
|
}
|
||||||
|
}
|
||||||
46
old code/tray/src/qz/installer/provision/invoker/ConfInvoker.java
Executable file
46
old code/tray/src/qz/installer/provision/invoker/ConfInvoker.java
Executable file
@@ -0,0 +1,46 @@
|
|||||||
|
package qz.installer.provision.invoker;
|
||||||
|
|
||||||
|
import qz.build.provision.Step;
|
||||||
|
import qz.common.PropertyHelper;
|
||||||
|
import qz.utils.SystemUtilities;
|
||||||
|
|
||||||
|
import java.util.AbstractMap;
|
||||||
|
|
||||||
|
public class ConfInvoker extends PropertyInvoker {
|
||||||
|
public ConfInvoker(Step step) {
|
||||||
|
super(step, new PropertyHelper(calculateConfPath(step)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String calculateConfPath(Step step) {
|
||||||
|
String relativePath = step.getArgs().get(0);
|
||||||
|
if(SystemUtilities.isMac()) {
|
||||||
|
return SystemUtilities.getJarParentPath().
|
||||||
|
resolve("../PlugIns/Java.runtime/Contents/Home/conf").
|
||||||
|
resolve(relativePath).
|
||||||
|
normalize()
|
||||||
|
.toString();
|
||||||
|
} else {
|
||||||
|
return SystemUtilities.getJarParentPath()
|
||||||
|
.resolve("runtime/conf")
|
||||||
|
.resolve(relativePath)
|
||||||
|
.normalize()
|
||||||
|
.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean invoke() {
|
||||||
|
Step step = getStep();
|
||||||
|
// Java uses the same "|" delimiter as we do, only parse one property at a time
|
||||||
|
AbstractMap.SimpleEntry<String, String> pair = parsePropertyPair(step, step.getData());
|
||||||
|
if (!pair.getValue().isEmpty()) {
|
||||||
|
properties.setProperty(pair);
|
||||||
|
if (properties.save()) {
|
||||||
|
log.info("Successfully provisioned '1' '{}'", step.getType());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
log.error("An error occurred saving properties '{}' to file", step.getData());
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
10
old code/tray/src/qz/installer/provision/invoker/Invokable.java
Executable file
10
old code/tray/src/qz/installer/provision/invoker/Invokable.java
Executable file
@@ -0,0 +1,10 @@
|
|||||||
|
package qz.installer.provision.invoker;
|
||||||
|
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
|
||||||
|
public interface Invokable {
|
||||||
|
Logger log = LogManager.getLogger(Invokable.class);
|
||||||
|
|
||||||
|
boolean invoke() throws Exception;
|
||||||
|
}
|
||||||
63
old code/tray/src/qz/installer/provision/invoker/InvokableResource.java
Executable file
63
old code/tray/src/qz/installer/provision/invoker/InvokableResource.java
Executable file
@@ -0,0 +1,63 @@
|
|||||||
|
package qz.installer.provision.invoker;
|
||||||
|
|
||||||
|
import org.apache.commons.io.IOUtils;
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
import qz.build.provision.Step;
|
||||||
|
import qz.build.provision.params.Type;
|
||||||
|
import qz.common.Constants;
|
||||||
|
import qz.utils.SystemUtilities;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.nio.file.StandardCopyOption;
|
||||||
|
|
||||||
|
public abstract class InvokableResource implements Invokable {
|
||||||
|
static final Logger log = LogManager.getLogger(InvokableResource.class);
|
||||||
|
|
||||||
|
public static File dataToFile(Step step) throws IOException {
|
||||||
|
Path resourcePath = Paths.get(step.getData());
|
||||||
|
if(resourcePath.isAbsolute() || step.usingPath()) {
|
||||||
|
return pathResourceToFile(step);
|
||||||
|
}
|
||||||
|
if(step.usingClass()) {
|
||||||
|
return classResourceToFile(step);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves the resource directly from file
|
||||||
|
*/
|
||||||
|
private static File pathResourceToFile(Step step) {
|
||||||
|
String resourcePath = step.getData();
|
||||||
|
Path dataPath = Paths.get(resourcePath);
|
||||||
|
return dataPath.isAbsolute() ? dataPath.toFile() : step.getRelativePath().resolve(resourcePath).toFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copies resource from JAR to a temp file for use in installation
|
||||||
|
*/
|
||||||
|
private static File classResourceToFile(Step step) throws IOException {
|
||||||
|
// Resource may be inside the jar
|
||||||
|
InputStream in = step.getRelativeClass().getResourceAsStream("resources/" + step.getData());
|
||||||
|
if(in == null) {
|
||||||
|
log.warn("Resource '{}' is missing, skipping step", step.getData());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String suffix = "_" + Paths.get(step.getData()).getFileName().toString();
|
||||||
|
File destination = File.createTempFile(Constants.DATA_DIR + "_provision_", suffix);
|
||||||
|
Files.copy(in, destination.toPath(), StandardCopyOption.REPLACE_EXISTING);
|
||||||
|
IOUtils.closeQuietly(in);
|
||||||
|
|
||||||
|
// Set scripts executable
|
||||||
|
if(step.getType() == Type.SCRIPT && !SystemUtilities.isWindows()) {
|
||||||
|
destination.setExecutable(true, false);
|
||||||
|
}
|
||||||
|
return destination;
|
||||||
|
}
|
||||||
|
}
|
||||||
99
old code/tray/src/qz/installer/provision/invoker/PropertyInvoker.java
Executable file
99
old code/tray/src/qz/installer/provision/invoker/PropertyInvoker.java
Executable file
@@ -0,0 +1,99 @@
|
|||||||
|
package qz.installer.provision.invoker;
|
||||||
|
|
||||||
|
import qz.build.provision.Step;
|
||||||
|
import qz.common.Constants;
|
||||||
|
import qz.common.PropertyHelper;
|
||||||
|
import qz.utils.FileUtilities;
|
||||||
|
import qz.utils.SystemUtilities;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.AbstractMap;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class PropertyInvoker implements Invokable {
|
||||||
|
private Step step;
|
||||||
|
PropertyHelper properties;
|
||||||
|
|
||||||
|
public PropertyInvoker(Step step, PropertyHelper properties) {
|
||||||
|
this.step = step;
|
||||||
|
this.properties = properties;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean invoke() {
|
||||||
|
HashMap<String, String> pairs = parsePropertyPairs(step);
|
||||||
|
if (!pairs.isEmpty()) {
|
||||||
|
for(Map.Entry<String, String> pair : pairs.entrySet()) {
|
||||||
|
properties.setProperty(pair);
|
||||||
|
}
|
||||||
|
if (properties.save()) {
|
||||||
|
log.info("Successfully provisioned '{}' '{}'", pairs.size(), step.getType());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
log.error("An error occurred saving properties '{}' to file", step.getData());
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PropertyHelper getProperties(Step step) {
|
||||||
|
File propertiesFile;
|
||||||
|
if(step.getRelativePath() != null) {
|
||||||
|
// Assume qz-tray.properties is one directory up from provision folder
|
||||||
|
// required to prevent installing to payload
|
||||||
|
propertiesFile = step.getRelativePath().getParent().resolve(Constants.PROPS_FILE + ".properties").toFile();
|
||||||
|
} else {
|
||||||
|
// If relative path isn't set, fallback to the jar's parent path
|
||||||
|
propertiesFile = SystemUtilities.getJarParentPath(".").resolve(Constants.PROPS_FILE + ".properties").toFile();
|
||||||
|
}
|
||||||
|
log.info("Provisioning '{}' to properties file: '{}'", step.getData(), propertiesFile);
|
||||||
|
return new PropertyHelper(propertiesFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PropertyHelper getPreferences(Step step) {
|
||||||
|
return new PropertyHelper(FileUtilities.USER_DIR + File.separator + Constants.PREFS_FILE + ".properties");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static HashMap<String, String> parsePropertyPairs(Step step) {
|
||||||
|
HashMap<String, String> pairs = new HashMap<>();
|
||||||
|
if(step.getData() != null && !step.getData().trim().isEmpty()) {
|
||||||
|
String[] props = step.getData().split("\\|");
|
||||||
|
for(String prop : props) {
|
||||||
|
AbstractMap.SimpleEntry<String,String> pair = parsePropertyPair(step, prop);
|
||||||
|
if (pair != null) {
|
||||||
|
if(pairs.get(pair.getKey()) != null) {
|
||||||
|
log.warn("Property {} already exists, replacing [before: {}, after: {}] ",
|
||||||
|
pair.getKey(), pairs.get(pair.getKey()), pair.getValue());
|
||||||
|
}
|
||||||
|
pairs.put(pair.getKey(), pair.getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.error("Skipping Step '{}', Data is null or empty", step.getType());
|
||||||
|
}
|
||||||
|
return pairs;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static AbstractMap.SimpleEntry<String, String> parsePropertyPair(Step step, String prop) {
|
||||||
|
if(prop.contains("=")) {
|
||||||
|
String[] pair = prop.split("=", 2);
|
||||||
|
if (!pair[0].trim().isEmpty()) {
|
||||||
|
if (!pair[1].trim().isEmpty()) {
|
||||||
|
return new AbstractMap.SimpleEntry<>(pair[0], pair[1]);
|
||||||
|
} else {
|
||||||
|
log.warn("Skipping '{}' '{}', property value is malformed", step.getType(), prop);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.warn("Skipping '{}' '{}', property name is malformed", step.getType(), prop);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.warn("Skipping '{}' '{}', property is malformed", step.getType(), prop);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Step getStep() {
|
||||||
|
return step;
|
||||||
|
}
|
||||||
|
}
|
||||||
100
old code/tray/src/qz/installer/provision/invoker/RemoverInvoker.java
Executable file
100
old code/tray/src/qz/installer/provision/invoker/RemoverInvoker.java
Executable file
@@ -0,0 +1,100 @@
|
|||||||
|
package qz.installer.provision.invoker;
|
||||||
|
|
||||||
|
import qz.build.provision.Step;
|
||||||
|
import qz.build.provision.params.Os;
|
||||||
|
import qz.build.provision.params.types.Remover;
|
||||||
|
import qz.utils.ShellUtilities;
|
||||||
|
import qz.utils.SystemUtilities;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
|
||||||
|
public class RemoverInvoker extends InvokableResource {
|
||||||
|
private Step step;
|
||||||
|
private String aboutTitle; // e.g. "QZ Tray"
|
||||||
|
private String propsFile; // e.g. "qz-tray"
|
||||||
|
private String dataDir; // e.g. "qz"
|
||||||
|
|
||||||
|
|
||||||
|
public RemoverInvoker(Step step) {
|
||||||
|
this.step = step;
|
||||||
|
Remover remover = Remover.parse(step.getData());
|
||||||
|
if(remover == Remover.CUSTOM) {
|
||||||
|
// Fields are comma delimited in the data field
|
||||||
|
parseCustomFromData(step.getData());
|
||||||
|
} else {
|
||||||
|
aboutTitle = remover.getAboutTitle();
|
||||||
|
propsFile = remover.getPropsFile();
|
||||||
|
dataDir = remover.getDataDir();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean invoke() throws Exception {
|
||||||
|
ArrayList<String> command = getRemoveCommand();
|
||||||
|
if(command.size() == 0) {
|
||||||
|
log.info("An existing installation of '{}' was not found. Skipping.", aboutTitle);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
boolean success = ShellUtilities.execute(command.toArray(new String[command.size()]));
|
||||||
|
if(!success) {
|
||||||
|
log.error("An error occurred invoking [{}]", step.getData());
|
||||||
|
}
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void parseCustomFromData(String data) {
|
||||||
|
String[] parts = data.split(",");
|
||||||
|
aboutTitle = parts[0].trim();
|
||||||
|
propsFile = parts[1].trim();
|
||||||
|
dataDir = parts[2].trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the installer command (including the installer itself and if needed, arguments) to
|
||||||
|
* invoke the installer file
|
||||||
|
*/
|
||||||
|
public ArrayList<String> getRemoveCommand() {
|
||||||
|
ArrayList<String> removeCmd = new ArrayList<>();
|
||||||
|
Os os = SystemUtilities.getOs();
|
||||||
|
switch(os) {
|
||||||
|
case WINDOWS:
|
||||||
|
Path win = Paths.get(System.getenv("PROGRAMFILES"))
|
||||||
|
.resolve(aboutTitle)
|
||||||
|
.resolve("uninstall.exe");
|
||||||
|
|
||||||
|
if(win.toFile().exists()) {
|
||||||
|
removeCmd.add(win.toString());
|
||||||
|
removeCmd.add("/S");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case MAC:
|
||||||
|
Path legacy = Paths.get("/Applications")
|
||||||
|
.resolve(aboutTitle + ".app")
|
||||||
|
.resolve("Contents")
|
||||||
|
.resolve("uninstall");
|
||||||
|
|
||||||
|
Path mac = Paths.get("/Applications")
|
||||||
|
.resolve(aboutTitle + ".app")
|
||||||
|
.resolve("Contents")
|
||||||
|
.resolve("Resources")
|
||||||
|
.resolve("uninstall");
|
||||||
|
|
||||||
|
if(legacy.toFile().exists()) {
|
||||||
|
removeCmd.add(legacy.toString());
|
||||||
|
} else if(mac.toFile().exists()) {
|
||||||
|
removeCmd.add(mac.toString());
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
Path linux = Paths.get("/opt")
|
||||||
|
.resolve(propsFile)
|
||||||
|
.resolve("uninstall");
|
||||||
|
if(linux.toFile().exists()) {
|
||||||
|
removeCmd.add(linux.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return removeCmd;
|
||||||
|
}
|
||||||
|
}
|
||||||
19
old code/tray/src/qz/installer/provision/invoker/ResourceInvoker.java
Executable file
19
old code/tray/src/qz/installer/provision/invoker/ResourceInvoker.java
Executable file
@@ -0,0 +1,19 @@
|
|||||||
|
package qz.installer.provision.invoker;
|
||||||
|
|
||||||
|
import qz.build.provision.Step;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stub class for deploying an otherwise "action-less" resource, only to be used by other tasks
|
||||||
|
*/
|
||||||
|
public class ResourceInvoker extends InvokableResource {
|
||||||
|
private Step step;
|
||||||
|
|
||||||
|
public ResourceInvoker(Step step) {
|
||||||
|
this.step = step;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean invoke() throws Exception {
|
||||||
|
return dataToFile(step) != null;
|
||||||
|
}
|
||||||
|
}
|
||||||
77
old code/tray/src/qz/installer/provision/invoker/ScriptInvoker.java
Executable file
77
old code/tray/src/qz/installer/provision/invoker/ScriptInvoker.java
Executable file
@@ -0,0 +1,77 @@
|
|||||||
|
package qz.installer.provision.invoker;
|
||||||
|
|
||||||
|
import qz.build.provision.Step;
|
||||||
|
import qz.build.provision.params.Os;
|
||||||
|
import qz.build.provision.params.types.Script;
|
||||||
|
import qz.utils.ShellUtilities;
|
||||||
|
import qz.utils.SystemUtilities;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
|
||||||
|
public class ScriptInvoker extends InvokableResource {
|
||||||
|
private Step step;
|
||||||
|
|
||||||
|
public ScriptInvoker(Step step) {
|
||||||
|
this.step = step;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean invoke() throws Exception {
|
||||||
|
File script = dataToFile(step);
|
||||||
|
if(script == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Script engine = Script.parse(step.getData());
|
||||||
|
ArrayList<String> command = getInterpreter(engine);
|
||||||
|
if(command.isEmpty() && SystemUtilities.isWindows()) {
|
||||||
|
log.warn("No interpreter found for {}, skipping", step.getData());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
command.add(script.toString());
|
||||||
|
boolean success = ShellUtilities.execute(command.toArray(new String[command.size()]));
|
||||||
|
if(!success) {
|
||||||
|
log.error("An error occurred invoking [{}]", step.getData());
|
||||||
|
}
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the interpreter command (and if needed, arguments) to invoke the script file
|
||||||
|
*
|
||||||
|
* An empty array will fall back to Unix "shebang" notation, e.g. #!/usr/bin/python3
|
||||||
|
* which will allow the OS to select the correct interpreter for the given file
|
||||||
|
*
|
||||||
|
* No special attention is given to "shebang", behavior may differ between OSs
|
||||||
|
*/
|
||||||
|
private static ArrayList<String> getInterpreter(Script engine) {
|
||||||
|
ArrayList<String> interpreter = new ArrayList<>();
|
||||||
|
Os osType = SystemUtilities.getOs();
|
||||||
|
switch(engine) {
|
||||||
|
case PS1:
|
||||||
|
if(osType == Os.WINDOWS) {
|
||||||
|
interpreter.add("powershell.exe");
|
||||||
|
} else if(osType == Os.MAC) {
|
||||||
|
interpreter.add("/usr/local/bin/pwsh");
|
||||||
|
} else {
|
||||||
|
interpreter.add("pwsh");
|
||||||
|
}
|
||||||
|
interpreter.add("-File");
|
||||||
|
break;
|
||||||
|
case PY:
|
||||||
|
interpreter.add(osType == Os.WINDOWS ? "python3.exe" : "python3");
|
||||||
|
break;
|
||||||
|
case BAT:
|
||||||
|
interpreter.add(osType == Os.WINDOWS ? "cmd.exe" : "wineconsole");
|
||||||
|
break;
|
||||||
|
case RB:
|
||||||
|
interpreter.add(osType == Os.WINDOWS ? "ruby.exe" : "ruby");
|
||||||
|
break;
|
||||||
|
case SH:
|
||||||
|
default:
|
||||||
|
// Allow the environment to parse it from the shebang at invocation time
|
||||||
|
}
|
||||||
|
return interpreter;
|
||||||
|
}
|
||||||
|
}
|
||||||
87
old code/tray/src/qz/installer/provision/invoker/SoftwareInvoker.java
Executable file
87
old code/tray/src/qz/installer/provision/invoker/SoftwareInvoker.java
Executable file
@@ -0,0 +1,87 @@
|
|||||||
|
package qz.installer.provision.invoker;
|
||||||
|
|
||||||
|
import qz.build.provision.Step;
|
||||||
|
import qz.build.provision.params.Os;
|
||||||
|
import qz.build.provision.params.types.Software;
|
||||||
|
import qz.utils.ShellUtilities;
|
||||||
|
import qz.utils.SystemUtilities;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class SoftwareInvoker extends InvokableResource {
|
||||||
|
private Step step;
|
||||||
|
|
||||||
|
public SoftwareInvoker(Step step) {
|
||||||
|
this.step = step;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean invoke() throws Exception {
|
||||||
|
File payload = dataToFile(step);
|
||||||
|
if(payload == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Software installer = Software.parse(step.getData());
|
||||||
|
ArrayList<String> command = getInstallCommand(installer, step.getArgs(), payload);
|
||||||
|
boolean success = ShellUtilities.execute(command.toArray(new String[command.size()]), payload.getParentFile());
|
||||||
|
if(!success) {
|
||||||
|
log.error("An error occurred invoking [{}]", step.getData());
|
||||||
|
}
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the installer command (including the installer itself and if needed, arguments) to
|
||||||
|
* invoke the installer file
|
||||||
|
*/
|
||||||
|
public ArrayList<String> getInstallCommand(Software installer, List<String> args, File payload) {
|
||||||
|
ArrayList<String> interpreter = new ArrayList<>();
|
||||||
|
Os os = SystemUtilities.getOs();
|
||||||
|
switch(installer) {
|
||||||
|
case EXE:
|
||||||
|
if(!SystemUtilities.isWindows()) {
|
||||||
|
interpreter.add("wine");
|
||||||
|
}
|
||||||
|
// Executable on its own
|
||||||
|
interpreter.add(payload.toString());
|
||||||
|
interpreter.addAll(args); // Assume exe args come after payload
|
||||||
|
break;
|
||||||
|
case MSI:
|
||||||
|
interpreter.add(os == Os.WINDOWS ? "msiexec.exe" : "msiexec");
|
||||||
|
interpreter.add("/i"); // Assume standard install
|
||||||
|
interpreter.add(payload.toString());
|
||||||
|
interpreter.addAll(args); // Assume msiexec args come after payload
|
||||||
|
break;
|
||||||
|
case PKG:
|
||||||
|
if(os == Os.MAC) {
|
||||||
|
interpreter.add("installer");
|
||||||
|
interpreter.addAll(args); // Assume installer args come before payload
|
||||||
|
interpreter.add("-package");
|
||||||
|
interpreter.add(payload.toString());
|
||||||
|
interpreter.add("-target");
|
||||||
|
interpreter.add("/"); // Assume we don't want this on a removable volume
|
||||||
|
} else {
|
||||||
|
throw new UnsupportedOperationException("PKG is not yet supported on this platform");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case DMG:
|
||||||
|
// DMG requires "hdiutil attach", but the mount point is unknown
|
||||||
|
throw new UnsupportedOperationException("DMG is not yet supported");
|
||||||
|
case RUN:
|
||||||
|
if(SystemUtilities.isWindows()) {
|
||||||
|
interpreter.add("bash");
|
||||||
|
interpreter.add("-c");
|
||||||
|
}
|
||||||
|
interpreter.add(payload.toString());
|
||||||
|
interpreter.addAll(args); // Assume run args come after payload
|
||||||
|
// Executable on its own
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// We'll try to parse it from the shebang just before invocation time
|
||||||
|
}
|
||||||
|
return interpreter;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
44
old code/tray/src/qz/installer/shortcut/LinuxShortcutCreator.java
Executable file
44
old code/tray/src/qz/installer/shortcut/LinuxShortcutCreator.java
Executable file
@@ -0,0 +1,44 @@
|
|||||||
|
/**
|
||||||
|
* @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.installer.shortcut;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
import qz.installer.LinuxInstaller;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Tres Finocchiaro
|
||||||
|
*/
|
||||||
|
class LinuxShortcutCreator extends ShortcutCreator {
|
||||||
|
|
||||||
|
private static final Logger log = LogManager.getLogger(LinuxShortcutCreator.class);
|
||||||
|
private static String DESKTOP = System.getProperty("user.home") + "/Desktop/";
|
||||||
|
|
||||||
|
public boolean canAutoStart() {
|
||||||
|
return Files.exists(Paths.get(LinuxInstaller.STARTUP_DIR, LinuxInstaller.SHORTCUT_NAME));
|
||||||
|
}
|
||||||
|
public void createDesktopShortcut() {
|
||||||
|
copyShortcut(LinuxInstaller.APP_LAUNCHER, DESKTOP);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void copyShortcut(String source, String target) {
|
||||||
|
try {
|
||||||
|
Files.copy(Paths.get(source), Paths.get(target));
|
||||||
|
} catch(IOException e) {
|
||||||
|
log.warn("Error creating shortcut {}", target, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
100
old code/tray/src/qz/installer/shortcut/MacShortcutCreator.java
Executable file
100
old code/tray/src/qz/installer/shortcut/MacShortcutCreator.java
Executable file
@@ -0,0 +1,100 @@
|
|||||||
|
/**
|
||||||
|
* @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.installer.shortcut;
|
||||||
|
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
import org.w3c.dom.Document;
|
||||||
|
import org.w3c.dom.Node;
|
||||||
|
import org.w3c.dom.NodeList;
|
||||||
|
import org.xml.sax.SAXException;
|
||||||
|
import qz.common.Constants;
|
||||||
|
import qz.utils.MacUtilities;
|
||||||
|
import qz.utils.SystemUtilities;
|
||||||
|
|
||||||
|
import javax.xml.parsers.DocumentBuilder;
|
||||||
|
import javax.xml.parsers.DocumentBuilderFactory;
|
||||||
|
import javax.xml.parsers.ParserConfigurationException;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Tres Finocchiaro
|
||||||
|
*/
|
||||||
|
class MacShortcutCreator extends ShortcutCreator {
|
||||||
|
|
||||||
|
private static final Logger log = LogManager.getLogger(MacShortcutCreator.class);
|
||||||
|
private static String SHORTCUT_PATH = System.getProperty("user.home") + "/Desktop/" + Constants.ABOUT_TITLE;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify LaunchAgents plist file exists and parse it to verify it's enabled
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public boolean canAutoStart() {
|
||||||
|
// plist is stored as io.qz.plist
|
||||||
|
Path plistPath = Paths.get("/Library/LaunchAgents", MacUtilities.getBundleId() + ".plist");
|
||||||
|
|
||||||
|
if (Files.exists(plistPath)) {
|
||||||
|
try {
|
||||||
|
DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
|
||||||
|
DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
|
||||||
|
Document doc = dBuilder.parse(plistPath.toFile());
|
||||||
|
doc.getDocumentElement().normalize();
|
||||||
|
|
||||||
|
NodeList dictList = doc.getElementsByTagName("dict");
|
||||||
|
|
||||||
|
// Loop to find "RunAtLoad" key, then the adjacent key
|
||||||
|
boolean foundItem = false;
|
||||||
|
if (dictList.getLength() > 0) {
|
||||||
|
NodeList children = dictList.item(0).getChildNodes();
|
||||||
|
for(int n = 0; n < children.getLength(); n++) {
|
||||||
|
Node item = children.item(n);
|
||||||
|
// Apple stores booleans as adjacent tags to their owner
|
||||||
|
if (foundItem) {
|
||||||
|
String nodeName = children.item(n).getNodeName();
|
||||||
|
log.debug("Found RunAtLoad value {}", nodeName);
|
||||||
|
return "true".equals(nodeName);
|
||||||
|
}
|
||||||
|
if (item.getNodeName().equals("key") && item.getTextContent().equals("RunAtLoad")) {
|
||||||
|
log.debug("Found RunAtLoad key in {}", plistPath);
|
||||||
|
foundItem = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.warn("RunAtLoad was not in plist {}, autostart will not work.", plistPath);
|
||||||
|
}
|
||||||
|
catch(SAXException | IOException | ParserConfigurationException e) {
|
||||||
|
log.warn("Error reading plist {}, autostart will not work.", plistPath, e);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.warn("No plist {} found, autostart will not work", plistPath);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void createDesktopShortcut() {
|
||||||
|
try {
|
||||||
|
new File(SHORTCUT_PATH).delete();
|
||||||
|
if(SystemUtilities.getJarParentPath().endsWith("Contents")) {
|
||||||
|
// We're probably running from an .app bundle
|
||||||
|
Files.createSymbolicLink(Paths.get(SHORTCUT_PATH), SystemUtilities.getAppPath());
|
||||||
|
} else {
|
||||||
|
// We're running from a mystery location, use the jar instead
|
||||||
|
Files.createSymbolicLink(Paths.get(SHORTCUT_PATH), SystemUtilities.getJarPath());
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch(IOException e) {
|
||||||
|
log.warn("Could not create desktop shortcut {}", SHORTCUT_PATH, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
41
old code/tray/src/qz/installer/shortcut/ShortcutCreator.java
Executable file
41
old code/tray/src/qz/installer/shortcut/ShortcutCreator.java
Executable file
@@ -0,0 +1,41 @@
|
|||||||
|
/**
|
||||||
|
* @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.installer.shortcut;
|
||||||
|
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
import qz.utils.SystemUtilities;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility class for creating, querying and removing startup shortcuts and
|
||||||
|
* desktop shortcuts.
|
||||||
|
*
|
||||||
|
* @author Tres Finocchiaro
|
||||||
|
*/
|
||||||
|
public abstract class ShortcutCreator {
|
||||||
|
private static ShortcutCreator instance;
|
||||||
|
protected static final Logger log = LogManager.getLogger(ShortcutCreator.class);
|
||||||
|
public abstract boolean canAutoStart();
|
||||||
|
public abstract void createDesktopShortcut();
|
||||||
|
|
||||||
|
public static ShortcutCreator getInstance() {
|
||||||
|
if (instance == null) {
|
||||||
|
if (SystemUtilities.isWindows()) {
|
||||||
|
instance = new WindowsShortcutCreator();
|
||||||
|
} else if (SystemUtilities.isMac()) {
|
||||||
|
instance = new MacShortcutCreator();
|
||||||
|
} else {
|
||||||
|
instance = new LinuxShortcutCreator();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
}
|
||||||
60
old code/tray/src/qz/installer/shortcut/WindowsShortcutCreator.java
Executable file
60
old code/tray/src/qz/installer/shortcut/WindowsShortcutCreator.java
Executable file
@@ -0,0 +1,60 @@
|
|||||||
|
/**
|
||||||
|
* @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.installer.shortcut;
|
||||||
|
|
||||||
|
import com.sun.jna.platform.win32.Win32Exception;
|
||||||
|
import mslinks.ShellLinkException;
|
||||||
|
import mslinks.ShellLinkHelper;
|
||||||
|
import qz.common.Constants;
|
||||||
|
import qz.installer.WindowsSpecialFolders;
|
||||||
|
import qz.utils.SystemUtilities;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Tres Finocchiaro
|
||||||
|
*/
|
||||||
|
public class WindowsShortcutCreator extends ShortcutCreator {
|
||||||
|
private static String SHORTCUT_NAME = Constants.ABOUT_TITLE + ".lnk";
|
||||||
|
|
||||||
|
public void createDesktopShortcut() {
|
||||||
|
createShortcut(WindowsSpecialFolders.DESKTOP.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean canAutoStart() {
|
||||||
|
try {
|
||||||
|
return Files.exists(Paths.get(WindowsSpecialFolders.COMMON_STARTUP.toString(), SHORTCUT_NAME));
|
||||||
|
} catch(Win32Exception e) {
|
||||||
|
log.warn("An exception occurred locating the startup folder; autostart cannot be determined.", e);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createShortcut(String folderPath) {
|
||||||
|
try {
|
||||||
|
ShellLinkHelper.createLink(getAppPath(), folderPath + File.separator + SHORTCUT_NAME);
|
||||||
|
}
|
||||||
|
catch(ShellLinkException | IOException ex) {
|
||||||
|
log.warn("Error creating desktop shortcut", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates .exe path from .jar
|
||||||
|
* fixme: overlaps SystemUtilities.getAppPath
|
||||||
|
*/
|
||||||
|
private static String getAppPath() {
|
||||||
|
return SystemUtilities.getJarPath().toString().replaceAll(".jar$", ".exe");
|
||||||
|
}
|
||||||
|
}
|
||||||
752
old code/tray/src/qz/printer/PrintOptions.java
Executable file
752
old code/tray/src/qz/printer/PrintOptions.java
Executable file
@@ -0,0 +1,752 @@
|
|||||||
|
package qz.printer;
|
||||||
|
|
||||||
|
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.utils.LoggerUtilities;
|
||||||
|
import qz.utils.PrintingUtilities;
|
||||||
|
import qz.utils.SystemUtilities;
|
||||||
|
|
||||||
|
import javax.print.attribute.ResolutionSyntax;
|
||||||
|
import javax.print.attribute.Size2DSyntax;
|
||||||
|
import javax.print.attribute.standard.Chromaticity;
|
||||||
|
import javax.print.attribute.standard.OrientationRequested;
|
||||||
|
import javax.print.attribute.standard.PrinterResolution;
|
||||||
|
import javax.print.attribute.standard.Sides;
|
||||||
|
import java.awt.*;
|
||||||
|
import java.awt.print.PageFormat;
|
||||||
|
import java.awt.print.PrinterException;
|
||||||
|
import java.awt.print.PrinterJob;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
public class PrintOptions {
|
||||||
|
|
||||||
|
private static final Logger log = LogManager.getLogger(PrintOptions.class);
|
||||||
|
|
||||||
|
private Pixel psOptions = new Pixel();
|
||||||
|
private Raw rawOptions = new Raw();
|
||||||
|
private Default defOptions = new Default();
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the provided JSON Object into relevant Pixel and Raw options
|
||||||
|
*/
|
||||||
|
public PrintOptions(JSONObject configOpts, PrintOutput output, PrintingUtilities.Format format) {
|
||||||
|
if (configOpts == null) { return; }
|
||||||
|
|
||||||
|
//check for raw options
|
||||||
|
if (!configOpts.isNull("forceRaw")) {
|
||||||
|
rawOptions.forceRaw = configOpts.optBoolean("forceRaw", false);
|
||||||
|
} else if (!configOpts.isNull("altPrinting")) {
|
||||||
|
log.warn("Raw option \"altPrinting\" is deprecated. Please use \"forceRaw\" instead.");
|
||||||
|
rawOptions.forceRaw = configOpts.optBoolean("altPrinting", false);
|
||||||
|
}
|
||||||
|
if (rawOptions.forceRaw && SystemUtilities.isWindows()) {
|
||||||
|
log.warn("Forced raw printing is not supported on Windows");
|
||||||
|
rawOptions.forceRaw = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!configOpts.isNull("encoding")) {
|
||||||
|
JSONObject encodings = configOpts.optJSONObject("encoding");
|
||||||
|
if (encodings != null) {
|
||||||
|
rawOptions.srcEncoding = encodings.optString("from", null);
|
||||||
|
rawOptions.destEncoding = encodings.optString("to", null);
|
||||||
|
} else {
|
||||||
|
rawOptions.destEncoding = configOpts.optString("encoding", null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!configOpts.isNull("spool")) {
|
||||||
|
JSONObject spool = configOpts.optJSONObject("spool");
|
||||||
|
if (spool != null) {
|
||||||
|
if (!spool.isNull("size")) {
|
||||||
|
try { rawOptions.spoolSize = spool.getInt("size"); }
|
||||||
|
catch(JSONException e) { LoggerUtilities.optionWarn(log, "integer", "spool.size", spool.opt("size")); }
|
||||||
|
}
|
||||||
|
// TODO: Implement spool.start
|
||||||
|
if (!spool.isNull("end")) {
|
||||||
|
rawOptions.spoolEnd = spool.optString("end");
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
LoggerUtilities.optionWarn(log, "JSONObject", "spool", configOpts.opt("spool"));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Deprecated
|
||||||
|
if (!configOpts.isNull("perSpool")) {
|
||||||
|
try { rawOptions.spoolSize = configOpts.getInt("perSpool"); }
|
||||||
|
catch(JSONException e) { LoggerUtilities.optionWarn(log, "integer", "perSpool", configOpts.opt("perSpool")); }
|
||||||
|
}
|
||||||
|
if (!configOpts.isNull("endOfDoc")) {
|
||||||
|
rawOptions.spoolEnd = configOpts.optString("endOfDoc", null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!configOpts.isNull("copies")) {
|
||||||
|
try { rawOptions.copies = configOpts.getInt("copies"); }
|
||||||
|
catch(JSONException e) { LoggerUtilities.optionWarn(log, "integer", "copies", configOpts.opt("copies")); }
|
||||||
|
}
|
||||||
|
if (!configOpts.isNull("jobName")) {
|
||||||
|
rawOptions.jobName = configOpts.optString("jobName", null);
|
||||||
|
}
|
||||||
|
if (!configOpts.isNull("retainTemp")) {
|
||||||
|
rawOptions.retainTemp = configOpts.optBoolean("retainTemp", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//check for pixel options
|
||||||
|
if (!configOpts.isNull("units")) {
|
||||||
|
switch(configOpts.optString("units")) {
|
||||||
|
case "mm":
|
||||||
|
psOptions.units = Unit.MM; break;
|
||||||
|
case "cm":
|
||||||
|
psOptions.units = Unit.CM; break;
|
||||||
|
case "in":
|
||||||
|
psOptions.units = Unit.INCH; break;
|
||||||
|
default:
|
||||||
|
LoggerUtilities.optionWarn(log, "valid value", "units", configOpts.opt("units")); break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!configOpts.isNull("bounds")) {
|
||||||
|
try {
|
||||||
|
JSONObject bounds = configOpts.getJSONObject("bounds");
|
||||||
|
psOptions.bounds = new Bounds(bounds.optDouble("x", 0), bounds.optDouble("y", 0), bounds.optDouble("width", 0), bounds.optDouble("height", 0));
|
||||||
|
}
|
||||||
|
catch(JSONException e) {
|
||||||
|
LoggerUtilities.optionWarn(log, "JSONObject", "bounds", configOpts.opt("bounds"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!configOpts.isNull("colorType")) {
|
||||||
|
try {
|
||||||
|
psOptions.colorType = ColorType.valueOf(configOpts.optString("colorType").toUpperCase(Locale.ENGLISH));
|
||||||
|
}
|
||||||
|
catch(IllegalArgumentException e) {
|
||||||
|
LoggerUtilities.optionWarn(log, "valid value", "colorType", configOpts.opt("colorType"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!configOpts.isNull("copies")) {
|
||||||
|
try { psOptions.copies = configOpts.getInt("copies"); }
|
||||||
|
catch(JSONException e) { LoggerUtilities.optionWarn(log, "integer", "copies", configOpts.opt("copies")); }
|
||||||
|
if (psOptions.copies < 1) {
|
||||||
|
log.warn("Cannot have less than one copy");
|
||||||
|
psOptions.copies = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!configOpts.isNull("density")) {
|
||||||
|
JSONObject asymmDPI = configOpts.optJSONObject("density");
|
||||||
|
if (asymmDPI != null) {
|
||||||
|
psOptions.density = asymmDPI.optInt("feed");
|
||||||
|
psOptions.crossDensity = asymmDPI.optInt("cross");
|
||||||
|
} else {
|
||||||
|
List<PrinterResolution> rSupport = output.isSetService()?
|
||||||
|
output.getNativePrinter().getResolutions():new ArrayList<>();
|
||||||
|
|
||||||
|
JSONArray possibleDPIs = configOpts.optJSONArray("density");
|
||||||
|
if (possibleDPIs != null && possibleDPIs.length() > 0) {
|
||||||
|
PrinterResolution usableRes = null;
|
||||||
|
|
||||||
|
if (!rSupport.isEmpty()) {
|
||||||
|
for(int i = 0; i < possibleDPIs.length(); i++) {
|
||||||
|
PrinterResolution compareRes;
|
||||||
|
asymmDPI = possibleDPIs.optJSONObject(i);
|
||||||
|
if (asymmDPI != null) {
|
||||||
|
compareRes = new PrinterResolution(asymmDPI.optInt("cross"), asymmDPI.optInt("feed"), psOptions.units.resSyntax);
|
||||||
|
} else {
|
||||||
|
compareRes = new PrinterResolution(possibleDPIs.optInt(i), possibleDPIs.optInt(i), psOptions.units.resSyntax);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rSupport.contains(compareRes)) {
|
||||||
|
usableRes = compareRes;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (usableRes == null) {
|
||||||
|
log.warn("Supported printer densities not found, using first value provided");
|
||||||
|
asymmDPI = possibleDPIs.optJSONObject(0);
|
||||||
|
if (asymmDPI != null) {
|
||||||
|
psOptions.density = asymmDPI.optInt("feed");
|
||||||
|
psOptions.crossDensity = asymmDPI.optInt("cross");
|
||||||
|
} else {
|
||||||
|
psOptions.density = possibleDPIs.optInt(0);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
psOptions.density = usableRes.getFeedResolution(psOptions.units.resSyntax);
|
||||||
|
psOptions.crossDensity = usableRes.getCrossFeedResolution(psOptions.units.resSyntax);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
String relDPI = configOpts.optString("density", "").toLowerCase(Locale.ENGLISH);
|
||||||
|
if ("best".equals(relDPI)) {
|
||||||
|
PrinterResolution bestRes = null;
|
||||||
|
for(PrinterResolution pr : rSupport) {
|
||||||
|
if (bestRes == null || !pr.lessThanOrEquals(bestRes)) {
|
||||||
|
bestRes = pr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (bestRes != null) {
|
||||||
|
psOptions.density = bestRes.getFeedResolution(psOptions.units.resSyntax);
|
||||||
|
psOptions.crossDensity = bestRes.getCrossFeedResolution(psOptions.units.resSyntax);
|
||||||
|
} else {
|
||||||
|
log.warn("No print densities were found; density: \"{}\" is being ignored", relDPI);
|
||||||
|
}
|
||||||
|
} else if ("draft".equals(relDPI)) {
|
||||||
|
PrinterResolution lowestRes = null;
|
||||||
|
for(PrinterResolution pr : rSupport) {
|
||||||
|
if (lowestRes == null || pr.lessThanOrEquals(lowestRes)) {
|
||||||
|
lowestRes = pr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (lowestRes != null) {
|
||||||
|
psOptions.density = lowestRes.getFeedResolution(psOptions.units.resSyntax);
|
||||||
|
psOptions.crossDensity = lowestRes.getCrossFeedResolution(psOptions.units.resSyntax);
|
||||||
|
} else {
|
||||||
|
log.warn("No print densities were found; density: \"{}\" is being ignored", relDPI);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
try { psOptions.density = configOpts.getDouble("density"); }
|
||||||
|
catch(JSONException e) { LoggerUtilities.optionWarn(log, "double", "density", configOpts.opt("density")); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!configOpts.isNull("dithering")) {
|
||||||
|
try {
|
||||||
|
if (configOpts.getBoolean("dithering")) {
|
||||||
|
psOptions.dithering = RenderingHints.VALUE_DITHER_ENABLE;
|
||||||
|
} else {
|
||||||
|
psOptions.dithering = RenderingHints.VALUE_DITHER_DISABLE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(JSONException e) { LoggerUtilities.optionWarn(log, "boolean", "dithering", configOpts.opt("dithering")); }
|
||||||
|
}
|
||||||
|
if (!configOpts.isNull("duplex")) {
|
||||||
|
try {
|
||||||
|
if (configOpts.getBoolean("duplex")) {
|
||||||
|
psOptions.duplex = Sides.DUPLEX;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(JSONException e) {
|
||||||
|
//not a boolean, try as a string
|
||||||
|
try {
|
||||||
|
String duplex = configOpts.getString("duplex").toLowerCase(Locale.ENGLISH);
|
||||||
|
if (duplex.matches("^(duplex|(two.sided.)?long(.edge)?)$")) {
|
||||||
|
psOptions.duplex = Sides.DUPLEX;
|
||||||
|
} else if (duplex.matches("^(tumble|(two.sided.)?short(.edge)?)$")) {
|
||||||
|
psOptions.duplex = Sides.TUMBLE;
|
||||||
|
}
|
||||||
|
//else - one sided (default)
|
||||||
|
}
|
||||||
|
catch(JSONException e2) { LoggerUtilities.optionWarn(log, "valid value", "duplex", configOpts.opt("duplex")); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!configOpts.isNull("interpolation")) {
|
||||||
|
switch(configOpts.optString("interpolation")) {
|
||||||
|
case "bicubic":
|
||||||
|
psOptions.interpolation = RenderingHints.VALUE_INTERPOLATION_BICUBIC; break;
|
||||||
|
case "bilinear":
|
||||||
|
psOptions.interpolation = RenderingHints.VALUE_INTERPOLATION_BILINEAR; break;
|
||||||
|
case "nearest-neighbor":
|
||||||
|
case "nearest":
|
||||||
|
psOptions.interpolation = RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR; break;
|
||||||
|
default:
|
||||||
|
LoggerUtilities.optionWarn(log, "valid value", "interpolation", configOpts.opt("interpolation")); break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!configOpts.isNull("jobName")) {
|
||||||
|
psOptions.jobName = configOpts.optString("jobName", null);
|
||||||
|
}
|
||||||
|
if (!configOpts.isNull("legacy")) {
|
||||||
|
psOptions.legacy = configOpts.optBoolean("legacy", false);
|
||||||
|
}
|
||||||
|
if (!configOpts.isNull("margins")) {
|
||||||
|
Margins m = new Margins();
|
||||||
|
JSONObject subMargins = configOpts.optJSONObject("margins");
|
||||||
|
if (subMargins != null) {
|
||||||
|
//each individually
|
||||||
|
if (!subMargins.isNull("top")) {
|
||||||
|
try { m.top = subMargins.getDouble("top"); }
|
||||||
|
catch(JSONException e) { LoggerUtilities.optionWarn(log, "double", "margins.top", subMargins.opt("top")); }
|
||||||
|
}
|
||||||
|
if (!subMargins.isNull("right")) {
|
||||||
|
try { m.right = subMargins.getDouble("right"); }
|
||||||
|
catch(JSONException e) { LoggerUtilities.optionWarn(log, "double", "margins.right", subMargins.opt("right")); }
|
||||||
|
}
|
||||||
|
if (!subMargins.isNull("bottom")) {
|
||||||
|
try { m.bottom = subMargins.getDouble("bottom"); }
|
||||||
|
catch(JSONException e) { LoggerUtilities.optionWarn(log, "double", "margins.bottom", subMargins.opt("bottom")); }
|
||||||
|
}
|
||||||
|
if (!subMargins.isNull("left")) {
|
||||||
|
try { m.left = subMargins.getDouble("left"); }
|
||||||
|
catch(JSONException e) { LoggerUtilities.optionWarn(log, "double", "margins.left", subMargins.opt("left")); }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
try { m.setAll(configOpts.getDouble("margins")); }
|
||||||
|
catch(JSONException e) { LoggerUtilities.optionWarn(log, "double", "margins", configOpts.opt("margins")); }
|
||||||
|
}
|
||||||
|
|
||||||
|
psOptions.margins = m;
|
||||||
|
}
|
||||||
|
if (!configOpts.isNull("orientation")) {
|
||||||
|
try {
|
||||||
|
psOptions.orientation = Orientation.valueOf(configOpts.optString("orientation").replaceAll("-", "_").toUpperCase(Locale.ENGLISH));
|
||||||
|
}
|
||||||
|
catch(IllegalArgumentException e) {
|
||||||
|
LoggerUtilities.optionWarn(log, "valid value", "orientation", configOpts.opt("orientation"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!configOpts.isNull("paperThickness")) {
|
||||||
|
try { psOptions.paperThickness = configOpts.getDouble("paperThickness"); }
|
||||||
|
catch(JSONException e) { LoggerUtilities.optionWarn(log, "double", "paperThickness", configOpts.opt("paperThickness")); }
|
||||||
|
}
|
||||||
|
if (!configOpts.isNull("spool")) {
|
||||||
|
JSONObject spool = configOpts.optJSONObject("spool");
|
||||||
|
if (spool != null) {
|
||||||
|
if (!spool.isNull("size")) {
|
||||||
|
try { psOptions.spoolSize = spool.getInt("size"); }
|
||||||
|
catch(JSONException e) { LoggerUtilities.optionWarn(log, "integer", "spool.size", spool.opt("size")); }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LoggerUtilities.optionWarn(log, "JSONObject", "spool", configOpts.opt("spool"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!configOpts.isNull("printerTray")) {
|
||||||
|
psOptions.printerTray = configOpts.optString("printerTray", null);
|
||||||
|
// Guard empty string value; will break pattern matching
|
||||||
|
if(psOptions.printerTray != null && psOptions.printerTray.trim().equals("")) {
|
||||||
|
psOptions.printerTray = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!configOpts.isNull("rasterize")) {
|
||||||
|
try { psOptions.rasterize = configOpts.getBoolean("rasterize"); }
|
||||||
|
catch(JSONException e) { LoggerUtilities.optionWarn(log, "boolean", "rasterize", configOpts.opt("rasterize")); }
|
||||||
|
}
|
||||||
|
if (!configOpts.isNull("rotation")) {
|
||||||
|
try { psOptions.rotation = configOpts.getDouble("rotation"); }
|
||||||
|
catch(JSONException e) { LoggerUtilities.optionWarn(log, "double", "rotation", configOpts.opt("rotation")); }
|
||||||
|
}
|
||||||
|
if (!configOpts.isNull("scaleContent")) {
|
||||||
|
try { psOptions.scaleContent = configOpts.getBoolean("scaleContent"); }
|
||||||
|
catch(JSONException e) { LoggerUtilities.optionWarn(log, "boolean", "scaleContent", configOpts.opt("scaleContent")); }
|
||||||
|
}
|
||||||
|
if (!configOpts.isNull("size")) {
|
||||||
|
Size s = new Size();
|
||||||
|
JSONObject subSize = configOpts.optJSONObject("size");
|
||||||
|
if (subSize != null) {
|
||||||
|
if (!subSize.isNull("width")) {
|
||||||
|
try { s.width = subSize.getDouble("width"); }
|
||||||
|
catch(JSONException e) { LoggerUtilities.optionWarn(log, "double", "size.width", subSize.opt("width")); }
|
||||||
|
}
|
||||||
|
if (!subSize.isNull("height")) {
|
||||||
|
try { s.height = subSize.getDouble("height"); }
|
||||||
|
catch(JSONException e) { LoggerUtilities.optionWarn(log, "double", "size.height", subSize.opt("height")); }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (s.height <= 0 && s.width <= 0) {
|
||||||
|
log.warn("Page size has been set without dimensions, using default");
|
||||||
|
} else {
|
||||||
|
psOptions.size = s;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LoggerUtilities.optionWarn(log, "JSONObject", "size", configOpts.opt("size"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//grab any useful service defaults
|
||||||
|
PrinterResolution defaultRes = null;
|
||||||
|
if (output.isSetService()) {
|
||||||
|
defaultRes = output.getNativePrinter().getResolution().value();
|
||||||
|
|
||||||
|
if (defaultRes == null) {
|
||||||
|
//printer has no default resolution set, see if it is possible to pull anything
|
||||||
|
List<PrinterResolution> rSupport = output.getNativePrinter().getResolutions();
|
||||||
|
if (rSupport.size() > 0) {
|
||||||
|
defaultRes = rSupport.get(0);
|
||||||
|
log.warn("Default resolution for {} is missing, using fallback: {}", output.getNativePrinter().getName(), defaultRes);
|
||||||
|
} else {
|
||||||
|
log.warn("Default resolution for {} is missing, no fallback available.", output.getNativePrinter().getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (defaultRes != null) {
|
||||||
|
//convert dphi to unit-dependant density ourselves (to keep as double type)
|
||||||
|
defOptions.density = (double)defaultRes.getFeedResolution(1) / psOptions.getUnits().getDPIUnits();
|
||||||
|
} else {
|
||||||
|
try { defOptions.density = configOpts.getDouble("fallbackDensity"); }
|
||||||
|
catch(JSONException e) {
|
||||||
|
LoggerUtilities.optionWarn(log, "double", "fallbackDensity", configOpts.opt("fallbackDensity"));
|
||||||
|
//manually convert default dphi to a density value based on units
|
||||||
|
defOptions.density = 60000d / psOptions.getUnits().getDPIUnits();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ((psOptions.isRasterize() || format == PrintingUtilities.Format.IMAGE) && psOptions.getDensity() <= 1) {
|
||||||
|
psOptions.density = defOptions.density;
|
||||||
|
psOptions.crossDensity = defOptions.density;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (output.isSetService()) {
|
||||||
|
try {
|
||||||
|
PrinterJob job = PrinterJob.getPrinterJob();
|
||||||
|
job.setPrintService(output.getPrintService());
|
||||||
|
PageFormat page = job.getPageFormat(null);
|
||||||
|
defOptions.pageSize = new Size(page.getWidth(), page.getHeight());
|
||||||
|
}
|
||||||
|
catch(PrinterException e) {
|
||||||
|
log.warn("Unable to find the default paper size");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public Raw getRawOptions() {
|
||||||
|
return rawOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Pixel getPixelOptions() {
|
||||||
|
return psOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Default getDefaultOptions() { return defOptions; }
|
||||||
|
|
||||||
|
|
||||||
|
// Option groups //
|
||||||
|
|
||||||
|
/** Raw printing options */
|
||||||
|
public class Raw {
|
||||||
|
private boolean forceRaw = false; //Alternate printing for linux systems
|
||||||
|
private String destEncoding = null; //Text encoding / charset
|
||||||
|
private String srcEncoding = null; //Conversion text encoding
|
||||||
|
private String spoolEnd = null; //End of document character(s)
|
||||||
|
private int spoolSize = 1; //Pages per spool
|
||||||
|
private int copies = 1; //Job copies
|
||||||
|
private String jobName = null; //Job name
|
||||||
|
private boolean retainTemp = false; //Retain any temporary files
|
||||||
|
|
||||||
|
|
||||||
|
public boolean isForceRaw() {
|
||||||
|
return forceRaw;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDestEncoding() {
|
||||||
|
return destEncoding;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getSrcEncoding() {
|
||||||
|
return srcEncoding;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getSpoolEnd() {
|
||||||
|
return spoolEnd;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getSpoolSize() {
|
||||||
|
return spoolSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getCopies() {
|
||||||
|
return copies;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isRetainTemp() { return retainTemp; }
|
||||||
|
|
||||||
|
public String getJobName(String defaultVal) {
|
||||||
|
return jobName == null || jobName.isEmpty()? defaultVal:jobName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Pixel printing options */
|
||||||
|
public class Pixel {
|
||||||
|
private Bounds bounds = null; //Bounding box rectangle
|
||||||
|
private ColorType colorType = ColorType.COLOR; //Color / black&white
|
||||||
|
private int copies = 1; //Job copies
|
||||||
|
private double crossDensity = 0; //Cross feed density
|
||||||
|
private double density = 0; //Pixel density (DPI or DPMM), feed density if crossDensity is defined
|
||||||
|
private Object dithering = RenderingHints.VALUE_DITHER_DEFAULT; //Image dithering
|
||||||
|
private Sides duplex = Sides.ONE_SIDED; //Multi-siding
|
||||||
|
private Object interpolation = RenderingHints.VALUE_INTERPOLATION_BICUBIC; //Image interpolation
|
||||||
|
private String jobName = null; //Job name
|
||||||
|
private boolean legacy = false; //Legacy printing
|
||||||
|
private Margins margins = new Margins(); //Page margins
|
||||||
|
private Orientation orientation = null; //Page orientation
|
||||||
|
private double paperThickness = -1; //Paper thickness
|
||||||
|
private int spoolSize = 0; //Pages before sending to printer
|
||||||
|
private String printerTray = null; //Printer tray to use
|
||||||
|
private boolean rasterize = true; //Whether documents are rasterized before printing
|
||||||
|
private double rotation = 0; //Image rotation
|
||||||
|
private boolean scaleContent = true; //Adjust paper size for best image fit
|
||||||
|
private Size size = null; //Paper size
|
||||||
|
private Unit units = Unit.INCH; //Units for density, margins, size
|
||||||
|
|
||||||
|
|
||||||
|
public Bounds getBounds() {
|
||||||
|
return bounds;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ColorType getColorType() {
|
||||||
|
return colorType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getCopies() {
|
||||||
|
return copies;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getCrossDensity() {
|
||||||
|
return crossDensity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getDensity() {
|
||||||
|
return density;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Object getDithering() {
|
||||||
|
return dithering;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Sides getDuplex() {
|
||||||
|
return duplex;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Object getInterpolation() {
|
||||||
|
return interpolation;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getJobName(String defaultVal) {
|
||||||
|
return jobName == null || jobName.isEmpty()? defaultVal:jobName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isLegacy() {
|
||||||
|
return legacy;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Margins getMargins() {
|
||||||
|
return margins;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Orientation getOrientation() {
|
||||||
|
return orientation;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getPaperThickness() {
|
||||||
|
return paperThickness;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getSpoolSize() {
|
||||||
|
return spoolSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPrinterTray() {
|
||||||
|
return printerTray;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isRasterize() {
|
||||||
|
return rasterize;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getRotation() {
|
||||||
|
return rotation;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isScaleContent() {
|
||||||
|
return scaleContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Size getSize() {
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Unit getUnits() {
|
||||||
|
return units;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** PrintService Defaults **/
|
||||||
|
public class Default {
|
||||||
|
private double density;
|
||||||
|
private Size pageSize;
|
||||||
|
|
||||||
|
|
||||||
|
public double getDensity() {
|
||||||
|
return density;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Size getPageSize() {
|
||||||
|
return pageSize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sub options //
|
||||||
|
|
||||||
|
/** Pixel page size options */
|
||||||
|
public class Size {
|
||||||
|
private double width = -1; //Page width
|
||||||
|
private double height = -1; //Page height
|
||||||
|
|
||||||
|
|
||||||
|
public Size() {}
|
||||||
|
|
||||||
|
public Size(double width, double height) {
|
||||||
|
this.width = width;
|
||||||
|
this.height = height;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getWidth() {
|
||||||
|
return width;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getHeight() {
|
||||||
|
return height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Pixel page margins options */
|
||||||
|
public class Margins {
|
||||||
|
private double top = 0; //Top page margin
|
||||||
|
private double right = 0; //Right page margin
|
||||||
|
private double bottom = 0; //Bottom page margin
|
||||||
|
private double left = 0; //Left page margin
|
||||||
|
|
||||||
|
private void setAll(double margin) {
|
||||||
|
top = margin;
|
||||||
|
right = margin;
|
||||||
|
bottom = margin;
|
||||||
|
left = margin;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public double top() {
|
||||||
|
return top;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double right() {
|
||||||
|
return right;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double bottom() {
|
||||||
|
return bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double left() {
|
||||||
|
return left;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Bounding box generic rectangle */
|
||||||
|
public class Bounds {
|
||||||
|
private double x;
|
||||||
|
private double y;
|
||||||
|
private double width;
|
||||||
|
private double height;
|
||||||
|
|
||||||
|
public Bounds(double x, double y, double width, double height) {
|
||||||
|
this.x = x;
|
||||||
|
this.y = y;
|
||||||
|
this.width = width;
|
||||||
|
this.height = height;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getX() {
|
||||||
|
return x;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getY() {
|
||||||
|
return y;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getWidth() {
|
||||||
|
return width;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getHeight() {
|
||||||
|
return height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Pixel dimension values */
|
||||||
|
public enum Unit {
|
||||||
|
INCH(ResolutionSyntax.DPI, 1.0f, 1.0f, Size2DSyntax.INCH), //1in = 1in
|
||||||
|
CM(ResolutionSyntax.DPCM, .3937f, 2.54f, 10000), //1cm = .3937in ; 1in = 2.54cm
|
||||||
|
MM(ResolutionSyntax.DPCM * 10, .03937f, 25.4f, Size2DSyntax.MM); //1mm = .03937in ; 1in = 25.4mm
|
||||||
|
|
||||||
|
private final float fromInch;
|
||||||
|
private final float toInch; //multiplicand to convert to inches
|
||||||
|
private final int resSyntax;
|
||||||
|
private final int µm;
|
||||||
|
|
||||||
|
Unit(int resSyntax, float toIN, float fromIN, int µm) {
|
||||||
|
toInch = toIN;
|
||||||
|
fromInch = fromIN;
|
||||||
|
this.resSyntax = resSyntax;
|
||||||
|
this.µm = µm;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float toInches() {
|
||||||
|
return toInch;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float as1Inch() {
|
||||||
|
return fromInch;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getDPIUnits() {
|
||||||
|
return resSyntax;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getMediaSizeUnits() {
|
||||||
|
return µm;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Pixel page orientation option */
|
||||||
|
public enum Orientation {
|
||||||
|
PORTRAIT(OrientationRequested.PORTRAIT, PageFormat.PORTRAIT, 0),
|
||||||
|
REVERSE_PORTRAIT(OrientationRequested.PORTRAIT, PageFormat.PORTRAIT, 180),
|
||||||
|
LANDSCAPE(OrientationRequested.LANDSCAPE, PageFormat.LANDSCAPE, 270),
|
||||||
|
REVERSE_LANDSCAPE(OrientationRequested.REVERSE_LANDSCAPE, PageFormat.REVERSE_LANDSCAPE, 90);
|
||||||
|
|
||||||
|
private final OrientationRequested orientationRequested;
|
||||||
|
private final int orientationFormat;
|
||||||
|
private final int degreesRot;
|
||||||
|
|
||||||
|
Orientation(OrientationRequested orientationRequested, int orientationFormat, int degreesRot) {
|
||||||
|
this.orientationRequested = orientationRequested;
|
||||||
|
this.orientationFormat = orientationFormat;
|
||||||
|
this.degreesRot = degreesRot;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public OrientationRequested getAsOrientRequested() {
|
||||||
|
return orientationRequested;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getAsOrientFormat() {
|
||||||
|
return orientationFormat;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getDegreesRot() {
|
||||||
|
return degreesRot;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Pixel page color option */
|
||||||
|
public enum ColorType {
|
||||||
|
COLOR(Chromaticity.COLOR),
|
||||||
|
GREYSCALE(Chromaticity.MONOCHROME),
|
||||||
|
GRAYSCALE(Chromaticity.MONOCHROME),
|
||||||
|
BLACKWHITE(Chromaticity.MONOCHROME),
|
||||||
|
DEFAULT(null);
|
||||||
|
|
||||||
|
private final Chromaticity chromatic;
|
||||||
|
|
||||||
|
ColorType(Chromaticity chromatic) {
|
||||||
|
this.chromatic = chromatic;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public Chromaticity getAsChromaticity() {
|
||||||
|
return chromatic;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
92
old code/tray/src/qz/printer/PrintOutput.java
Executable file
92
old code/tray/src/qz/printer/PrintOutput.java
Executable file
@@ -0,0 +1,92 @@
|
|||||||
|
package qz.printer;
|
||||||
|
|
||||||
|
import org.codehaus.jettison.json.JSONException;
|
||||||
|
import org.codehaus.jettison.json.JSONObject;
|
||||||
|
import qz.printer.info.NativePrinter;
|
||||||
|
import qz.utils.FileUtilities;
|
||||||
|
|
||||||
|
import javax.print.PrintService;
|
||||||
|
import javax.print.attribute.standard.Media;
|
||||||
|
import java.io.File;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
|
||||||
|
public class PrintOutput {
|
||||||
|
|
||||||
|
private NativePrinter printer = null;
|
||||||
|
|
||||||
|
private File file = null;
|
||||||
|
|
||||||
|
private String host = null;
|
||||||
|
private int port = -1;
|
||||||
|
|
||||||
|
|
||||||
|
public PrintOutput(JSONObject configPrinter) throws JSONException, IllegalArgumentException {
|
||||||
|
if (configPrinter == null) { return; }
|
||||||
|
|
||||||
|
if (configPrinter.has("name")) {
|
||||||
|
printer = PrintServiceMatcher.matchPrinter(configPrinter.getString("name"));
|
||||||
|
if (printer == null) {
|
||||||
|
throw new IllegalArgumentException("Cannot find printer with name \"" + configPrinter.getString("name") + "\"");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (configPrinter.has("file")) {
|
||||||
|
String filename = configPrinter.getString("file");
|
||||||
|
if (!FileUtilities.isGoodExtension(Paths.get(filename))) {
|
||||||
|
throw new IllegalArgumentException("Writing to file \"" + filename + "\" is denied for security reasons. (Prohibited file extension)");
|
||||||
|
} else if (FileUtilities.isBadPath(filename)) {
|
||||||
|
throw new IllegalArgumentException("Writing to file \"" + filename + "\" is denied for security reasons. (Prohibited directory name)");
|
||||||
|
} else {
|
||||||
|
file = new File(filename);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (configPrinter.has("host")) {
|
||||||
|
host = configPrinter.getString("host");
|
||||||
|
port = configPrinter.optInt("port", 9100); // default to port 9100 (HP/JetDirect standard) if not provided
|
||||||
|
}
|
||||||
|
|
||||||
|
//at least one method must be set for printing
|
||||||
|
if (!isSetService() && !isSetFile() && !isSetHost()) {
|
||||||
|
throw new IllegalArgumentException("No printer output has been specified");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public boolean isSetService() {
|
||||||
|
return printer != null && printer.getPrintService() != null && !printer.getPrintService().isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
public PrintService getPrintService() {
|
||||||
|
return printer.getPrintService().value();
|
||||||
|
}
|
||||||
|
|
||||||
|
public NativePrinter getNativePrinter() {
|
||||||
|
return printer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isSetFile() {
|
||||||
|
return file != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public File getFile() {
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isSetHost() {
|
||||||
|
return host != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getHost() {
|
||||||
|
return host;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getPort() {
|
||||||
|
return port;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Media[] getSupportedMedia() {
|
||||||
|
return (Media[])getPrintService().getSupportedAttributeValues(Media.class, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
242
old code/tray/src/qz/printer/PrintServiceMatcher.java
Executable file
242
old code/tray/src/qz/printer/PrintServiceMatcher.java
Executable file
@@ -0,0 +1,242 @@
|
|||||||
|
/**
|
||||||
|
* @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.printer;
|
||||||
|
|
||||||
|
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.printer.info.CachedPrintServiceLookup;
|
||||||
|
import qz.printer.info.NativePrinter;
|
||||||
|
import qz.printer.info.NativePrinterMap;
|
||||||
|
import qz.utils.SystemUtilities;
|
||||||
|
|
||||||
|
import javax.print.PrintService;
|
||||||
|
import javax.print.PrintServiceLookup;
|
||||||
|
import javax.print.attribute.ResolutionSyntax;
|
||||||
|
import javax.print.attribute.standard.*;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
public class PrintServiceMatcher {
|
||||||
|
private static final Logger log = LogManager.getLogger(PrintServiceMatcher.class);
|
||||||
|
|
||||||
|
// PrintService is slow in CUPS, use a cache instead per JDK-7001133
|
||||||
|
// TODO: Include JDK version test for caching when JDK-7001133 is fixed upstream
|
||||||
|
private static final boolean useCache = SystemUtilities.isUnix();
|
||||||
|
|
||||||
|
public static NativePrinterMap getNativePrinterList(boolean silent, boolean withAttributes) {
|
||||||
|
NativePrinterMap printers = NativePrinterMap.getInstance();
|
||||||
|
printers.putAll(true, lookupPrintServices());
|
||||||
|
if (withAttributes) { printers.values().forEach(NativePrinter::getDriverAttributes); }
|
||||||
|
if (!silent) { log.debug("Found {} printers", printers.size()); }
|
||||||
|
return printers;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PrintService[] lookupPrintServices() {
|
||||||
|
return useCache ? CachedPrintServiceLookup.lookupPrintServices() :
|
||||||
|
PrintServiceLookup.lookupPrintServices(null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PrintService lookupDefaultPrintService() {
|
||||||
|
return useCache ? CachedPrintServiceLookup.lookupDefaultPrintService() :
|
||||||
|
PrintServiceLookup.lookupDefaultPrintService();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static NativePrinterMap getNativePrinterList(boolean silent) {
|
||||||
|
return getNativePrinterList(silent, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static NativePrinterMap getNativePrinterList() {
|
||||||
|
return getNativePrinterList(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static NativePrinter getDefaultPrinter() {
|
||||||
|
PrintService defaultService = lookupDefaultPrintService();
|
||||||
|
|
||||||
|
if(defaultService == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
NativePrinterMap printers = NativePrinterMap.getInstance();
|
||||||
|
if (!printers.contains(defaultService)) {
|
||||||
|
printers.putAll(false, defaultService);
|
||||||
|
}
|
||||||
|
|
||||||
|
return printers.get(defaultService);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String findPrinterName(String query) throws JSONException {
|
||||||
|
NativePrinter printer = PrintServiceMatcher.matchPrinter(query);
|
||||||
|
|
||||||
|
if (printer != null) {
|
||||||
|
return printer.getPrintService().value().getName();
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds {@code PrintService} by looking at any matches to {@code printerSearch}.
|
||||||
|
*
|
||||||
|
* @param printerSearch Search query to compare against service names.
|
||||||
|
*/
|
||||||
|
public static NativePrinter matchPrinter(String printerSearch, boolean silent) {
|
||||||
|
NativePrinter exact = null;
|
||||||
|
NativePrinter begins = null;
|
||||||
|
NativePrinter partial = null;
|
||||||
|
|
||||||
|
if (!silent) { log.debug("Searching for PrintService matching {}", printerSearch); }
|
||||||
|
|
||||||
|
// Fix for https://github.com/qzind/tray/issues/931
|
||||||
|
// This is more than an optimization, removal will lead to a regression
|
||||||
|
NativePrinter defaultPrinter = getDefaultPrinter();
|
||||||
|
if (defaultPrinter != null && printerSearch.equals(defaultPrinter.getName())) {
|
||||||
|
if (!silent) { log.debug("Matched default printer, skipping further search"); }
|
||||||
|
return defaultPrinter;
|
||||||
|
}
|
||||||
|
|
||||||
|
printerSearch = printerSearch.toLowerCase(Locale.ENGLISH);
|
||||||
|
|
||||||
|
// Search services for matches
|
||||||
|
for(NativePrinter printer : getNativePrinterList(silent).values()) {
|
||||||
|
if (printer.getName() == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String printerName = printer.getName().toLowerCase(Locale.ENGLISH);
|
||||||
|
if (printerName.equals(printerSearch)) {
|
||||||
|
exact = printer;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (printerName.startsWith(printerSearch)) {
|
||||||
|
begins = printer;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (printerName.contains(printerSearch)) {
|
||||||
|
partial = printer;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (SystemUtilities.isMac()) {
|
||||||
|
// 1.9 compat: fallback for old style names
|
||||||
|
PrinterName name = printer.getLegacyName();
|
||||||
|
if (name == null || name.getValue() == null) { continue; }
|
||||||
|
printerName = name.getValue().toLowerCase(Locale.ENGLISH);
|
||||||
|
if (printerName.equals(printerSearch)) {
|
||||||
|
exact = printer;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (printerName.startsWith(printerSearch)) {
|
||||||
|
begins = printer;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (printerName.contains(printerSearch)) {
|
||||||
|
partial = printer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return closest match
|
||||||
|
NativePrinter use = null;
|
||||||
|
if (exact != null) {
|
||||||
|
use = exact;
|
||||||
|
} else if (begins != null) {
|
||||||
|
use = begins;
|
||||||
|
} else if (partial != null) {
|
||||||
|
use = partial;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (use != null) {
|
||||||
|
if(!silent) log.debug("Found match: {}", use.getPrintService().value().getName());
|
||||||
|
} else {
|
||||||
|
log.warn("Printer not found: {}", printerSearch);
|
||||||
|
}
|
||||||
|
|
||||||
|
return use;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static NativePrinter matchPrinter(String printerSearch) {
|
||||||
|
return matchPrinter(printerSearch, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static JSONArray getPrintersJSON(boolean includeDetails) throws JSONException {
|
||||||
|
JSONArray list = new JSONArray();
|
||||||
|
|
||||||
|
PrintService defaultService = lookupDefaultPrintService();
|
||||||
|
|
||||||
|
boolean mediaTrayCrawled = false;
|
||||||
|
|
||||||
|
for(NativePrinter printer : getNativePrinterList().values()) {
|
||||||
|
PrintService ps = printer.getPrintService().value();
|
||||||
|
JSONObject jsonService = new JSONObject();
|
||||||
|
jsonService.put("name", ps.getName());
|
||||||
|
|
||||||
|
if (includeDetails) {
|
||||||
|
jsonService.put("driver", printer.getDriver().value());
|
||||||
|
jsonService.put("connection", printer.getConnection());
|
||||||
|
jsonService.put("default", ps == defaultService);
|
||||||
|
|
||||||
|
if (!mediaTrayCrawled) {
|
||||||
|
log.info("Gathering printer MediaTray information...");
|
||||||
|
mediaTrayCrawled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
HashSet<String> uniqueSizes = new HashSet<>(); // prevents duplicates
|
||||||
|
JSONArray trays = new JSONArray();
|
||||||
|
JSONArray sizes = new JSONArray();
|
||||||
|
|
||||||
|
for(Media m : (Media[])ps.getSupportedAttributeValues(Media.class, null, null)) {
|
||||||
|
if (m instanceof MediaTray) { trays.put(m.toString()); }
|
||||||
|
if (m instanceof MediaSizeName) {
|
||||||
|
if(uniqueSizes.add(m.toString())) {
|
||||||
|
MediaSize mediaSize = MediaSize.getMediaSizeForName((MediaSizeName)m);
|
||||||
|
if(mediaSize == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
JSONObject size = new JSONObject();
|
||||||
|
size.put("name", m.toString());
|
||||||
|
|
||||||
|
JSONObject in = new JSONObject();
|
||||||
|
in.put("width", mediaSize.getX(MediaPrintableArea.INCH));
|
||||||
|
in.put("height", mediaSize.getY(MediaPrintableArea.INCH));
|
||||||
|
size.put("in", in);
|
||||||
|
|
||||||
|
JSONObject mm = new JSONObject();
|
||||||
|
mm.put("width", mediaSize.getX(MediaPrintableArea.MM));
|
||||||
|
mm.put("height", mediaSize.getY(MediaPrintableArea.MM));
|
||||||
|
size.put("mm", mm);
|
||||||
|
|
||||||
|
sizes.put(size);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(trays.length() > 0) {
|
||||||
|
jsonService.put("trays", trays);
|
||||||
|
}
|
||||||
|
if(sizes.length() > 0) {
|
||||||
|
jsonService.put("sizes", sizes);
|
||||||
|
}
|
||||||
|
|
||||||
|
PrinterResolution res = printer.getResolution().value();
|
||||||
|
int density = -1; if (res != null) { density = res.getFeedResolution(ResolutionSyntax.DPI); }
|
||||||
|
jsonService.put("density", density);
|
||||||
|
}
|
||||||
|
|
||||||
|
list.put(jsonService);
|
||||||
|
}
|
||||||
|
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
97
old code/tray/src/qz/printer/action/PrintDirect.java
Executable file
97
old code/tray/src/qz/printer/action/PrintDirect.java
Executable file
@@ -0,0 +1,97 @@
|
|||||||
|
package qz.printer.action;
|
||||||
|
|
||||||
|
import org.apache.commons.codec.binary.Base64InputStream;
|
||||||
|
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.common.Constants;
|
||||||
|
import qz.printer.PrintOptions;
|
||||||
|
import qz.printer.PrintOutput;
|
||||||
|
import qz.utils.PrintingUtilities;
|
||||||
|
|
||||||
|
import javax.print.DocFlavor;
|
||||||
|
import javax.print.DocPrintJob;
|
||||||
|
import javax.print.PrintException;
|
||||||
|
import javax.print.SimpleDoc;
|
||||||
|
import javax.print.attribute.HashPrintRequestAttributeSet;
|
||||||
|
import javax.print.attribute.PrintRequestAttributeSet;
|
||||||
|
import javax.print.attribute.standard.JobName;
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.DataInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
public class PrintDirect extends PrintRaw {
|
||||||
|
|
||||||
|
private static final Logger log = LogManager.getLogger(PrintDirect.class);
|
||||||
|
|
||||||
|
private ArrayList<String> prints = new ArrayList<>();
|
||||||
|
private ArrayList<PrintingUtilities.Flavor> flavors = new ArrayList<>();
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PrintingUtilities.Format getFormat() {
|
||||||
|
return PrintingUtilities.Format.DIRECT;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void parseData(JSONArray printData, PrintOptions options) throws JSONException, UnsupportedOperationException {
|
||||||
|
for(int i = 0; i < printData.length(); i++) {
|
||||||
|
JSONObject data = printData.optJSONObject(i);
|
||||||
|
if (data == null) { continue; }
|
||||||
|
|
||||||
|
prints.add(data.getString("data"));
|
||||||
|
flavors.add(PrintingUtilities.Flavor.parse(data, PrintingUtilities.Flavor.PLAIN));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void print(PrintOutput output, PrintOptions options) throws PrintException {
|
||||||
|
PrintRequestAttributeSet attributes = new HashPrintRequestAttributeSet();
|
||||||
|
attributes.add(new JobName(options.getRawOptions().getJobName(Constants.RAW_PRINT), Locale.getDefault()));
|
||||||
|
|
||||||
|
for(int i = 0; i < prints.size(); i++) {
|
||||||
|
DocPrintJob printJob = output.getPrintService().createPrintJob();
|
||||||
|
InputStream stream = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
switch(flavors.get(i)) {
|
||||||
|
case BASE64:
|
||||||
|
stream = new Base64InputStream(new ByteArrayInputStream(prints.get(i).getBytes("UTF-8")));
|
||||||
|
break;
|
||||||
|
case FILE:
|
||||||
|
stream = new DataInputStream(new URL(prints.get(i)).openStream());
|
||||||
|
break;
|
||||||
|
case PLAIN:
|
||||||
|
default:
|
||||||
|
stream = new ByteArrayInputStream(prints.get(i).getBytes("UTF-8"));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
SimpleDoc doc = new SimpleDoc(stream, DocFlavor.INPUT_STREAM.AUTOSENSE, null);
|
||||||
|
|
||||||
|
waitForPrint(printJob, doc, attributes);
|
||||||
|
}
|
||||||
|
catch(IOException e) {
|
||||||
|
throw new PrintException(e);
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
if (stream != null) {
|
||||||
|
try { stream.close(); } catch(Exception ignore) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void cleanup() {
|
||||||
|
prints.clear();
|
||||||
|
flavors.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
414
old code/tray/src/qz/printer/action/PrintHTML.java
Executable file
414
old code/tray/src/qz/printer/action/PrintHTML.java
Executable file
@@ -0,0 +1,414 @@
|
|||||||
|
/**
|
||||||
|
* @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.printer.action;
|
||||||
|
|
||||||
|
import com.sun.javafx.print.PrintHelper;
|
||||||
|
import com.sun.javafx.print.Units;
|
||||||
|
import javafx.print.*;
|
||||||
|
import org.apache.commons.io.IOUtils;
|
||||||
|
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.common.Constants;
|
||||||
|
import qz.printer.PrintOptions;
|
||||||
|
import qz.printer.PrintOutput;
|
||||||
|
import qz.printer.action.html.WebApp;
|
||||||
|
import qz.printer.action.html.WebAppModel;
|
||||||
|
import qz.utils.PrintingUtilities;
|
||||||
|
|
||||||
|
import javax.print.attribute.PrintRequestAttributeSet;
|
||||||
|
import javax.print.attribute.standard.Copies;
|
||||||
|
import javax.print.attribute.standard.CopiesSupported;
|
||||||
|
import javax.print.attribute.standard.Sides;
|
||||||
|
import javax.swing.*;
|
||||||
|
import java.awt.*;
|
||||||
|
import java.awt.print.PageFormat;
|
||||||
|
import java.awt.print.PrinterException;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.lang.reflect.Constructor;
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class PrintHTML extends PrintImage implements PrintProcessor {
|
||||||
|
|
||||||
|
private static final Logger log = LogManager.getLogger(PrintHTML.class);
|
||||||
|
|
||||||
|
private List<WebAppModel> models;
|
||||||
|
|
||||||
|
private JLabel legacyLabel = null;
|
||||||
|
|
||||||
|
|
||||||
|
public PrintHTML() {
|
||||||
|
super();
|
||||||
|
models = new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PrintingUtilities.Format getFormat() {
|
||||||
|
return PrintingUtilities.Format.HTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void parseData(JSONArray printData, PrintOptions options) throws JSONException, UnsupportedOperationException {
|
||||||
|
try {
|
||||||
|
PrintOptions.Pixel pxlOpts = options.getPixelOptions();
|
||||||
|
if (!pxlOpts.isLegacy()) {
|
||||||
|
WebApp.initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
for(int i = 0; i < printData.length(); i++) {
|
||||||
|
JSONObject data = printData.getJSONObject(i);
|
||||||
|
|
||||||
|
PrintingUtilities.Flavor flavor = PrintingUtilities.Flavor.parse(data, PrintingUtilities.Flavor.FILE);
|
||||||
|
|
||||||
|
String source;
|
||||||
|
switch(flavor) {
|
||||||
|
case FILE:
|
||||||
|
case PLAIN:
|
||||||
|
// We'll toggle between 'plain' and 'file' when we construct WebAppModel
|
||||||
|
source = data.getString("data");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
source = new String(flavor.read(data.getString("data")), StandardCharsets.UTF_8);
|
||||||
|
}
|
||||||
|
|
||||||
|
double pageZoom = (pxlOpts.getDensity() * pxlOpts.getUnits().as1Inch()) / 72.0;
|
||||||
|
if (pageZoom <= 1) { pageZoom = 1; }
|
||||||
|
|
||||||
|
double pageWidth = 0;
|
||||||
|
double pageHeight = 0;
|
||||||
|
double convertFactor = (72.0 / pxlOpts.getUnits().as1Inch());
|
||||||
|
|
||||||
|
boolean renderFromHeight = Arrays.asList(PrintOptions.Orientation.LANDSCAPE,
|
||||||
|
PrintOptions.Orientation.REVERSE_LANDSCAPE).contains(pxlOpts.getOrientation());
|
||||||
|
|
||||||
|
if (pxlOpts.getSize() != null) {
|
||||||
|
if (!renderFromHeight) {
|
||||||
|
pageWidth = pxlOpts.getSize().getWidth() * convertFactor;
|
||||||
|
} else {
|
||||||
|
pageWidth = pxlOpts.getSize().getHeight() * convertFactor;
|
||||||
|
}
|
||||||
|
} else if (options.getDefaultOptions().getPageSize() != null) {
|
||||||
|
if (!renderFromHeight) {
|
||||||
|
pageWidth = options.getDefaultOptions().getPageSize().getWidth();
|
||||||
|
} else {
|
||||||
|
pageWidth = options.getDefaultOptions().getPageSize().getHeight();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pxlOpts.getMargins() != null) {
|
||||||
|
PrintOptions.Margins margins = pxlOpts.getMargins();
|
||||||
|
if (!renderFromHeight || pxlOpts.isRasterize()) {
|
||||||
|
pageWidth -= (margins.left() + margins.right()) * convertFactor;
|
||||||
|
} else {
|
||||||
|
pageWidth -= (margins.top() + margins.bottom()) * convertFactor; //due to vector margin matching
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data.isNull("options")) {
|
||||||
|
JSONObject dataOpt = data.getJSONObject("options");
|
||||||
|
|
||||||
|
if (!dataOpt.isNull("pageWidth") && dataOpt.optDouble("pageWidth") > 0) {
|
||||||
|
pageWidth = dataOpt.optDouble("pageWidth") * convertFactor;
|
||||||
|
}
|
||||||
|
if (!dataOpt.isNull("pageHeight") && dataOpt.optDouble("pageHeight") > 0) {
|
||||||
|
pageHeight = dataOpt.optDouble("pageHeight") * convertFactor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
models.add(new WebAppModel(source, (flavor != PrintingUtilities.Flavor.FILE), pageWidth, pageHeight, pxlOpts.isScaleContent(), pageZoom));
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug("Parsed {} html records", models.size());
|
||||||
|
}
|
||||||
|
catch(IOException e) {
|
||||||
|
throw new UnsupportedOperationException("Unable to start JavaFX service", e);
|
||||||
|
}
|
||||||
|
catch(NoClassDefFoundError e) {
|
||||||
|
throw new UnsupportedOperationException("JavaFX libraries not found", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void print(PrintOutput output, PrintOptions options) throws PrinterException {
|
||||||
|
if (options.getPixelOptions().isLegacy()) {
|
||||||
|
printLegacy(output, options);
|
||||||
|
} else if (options.getPixelOptions().isRasterize()) {
|
||||||
|
//grab a snapshot of the pages for PrintImage instead of printing directly
|
||||||
|
for(WebAppModel model : models) {
|
||||||
|
try { images.add(WebApp.raster(model)); }
|
||||||
|
catch(Throwable t) {
|
||||||
|
if (model.getZoom() > 1 && t instanceof IllegalArgumentException) {
|
||||||
|
//probably a unrecognized image loader error, try at default zoom
|
||||||
|
try {
|
||||||
|
log.warn("Capture failed with increased zoom, attempting with default value");
|
||||||
|
model.setZoom(1);
|
||||||
|
images.add(WebApp.raster(model));
|
||||||
|
}
|
||||||
|
catch(Throwable tt) {
|
||||||
|
throw new PrinterException(tt.getMessage());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new PrinterException(t.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
super.print(output, options);
|
||||||
|
} else {
|
||||||
|
Printer fxPrinter = null;
|
||||||
|
for(Printer p : Printer.getAllPrinters()) {
|
||||||
|
if (p.getName().equals(output.getPrintService().getName())) {
|
||||||
|
fxPrinter = p;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (fxPrinter == null) {
|
||||||
|
throw new PrinterException("Cannot find printer under the JavaFX libraries");
|
||||||
|
}
|
||||||
|
|
||||||
|
PrinterJob job = PrinterJob.createPrinterJob(fxPrinter);
|
||||||
|
|
||||||
|
|
||||||
|
// apply option settings
|
||||||
|
PrintOptions.Pixel pxlOpts = options.getPixelOptions();
|
||||||
|
JobSettings settings = job.getJobSettings();
|
||||||
|
settings.setJobName(pxlOpts.getJobName(Constants.HTML_PRINT));
|
||||||
|
settings.setPrintQuality(PrintQuality.HIGH);
|
||||||
|
|
||||||
|
// If colortype is default, leave printColor blank. The system's printer settings will be used instead.
|
||||||
|
if (pxlOpts.getColorType() != PrintOptions.ColorType.DEFAULT) {
|
||||||
|
settings.setPrintColor(getColor(pxlOpts));
|
||||||
|
}
|
||||||
|
if (pxlOpts.getDuplex() == Sides.DUPLEX || pxlOpts.getDuplex() == Sides.TWO_SIDED_LONG_EDGE) {
|
||||||
|
settings.setPrintSides(PrintSides.DUPLEX);
|
||||||
|
}
|
||||||
|
if (pxlOpts.getDuplex() == Sides.TUMBLE || pxlOpts.getDuplex() == Sides.TWO_SIDED_SHORT_EDGE) {
|
||||||
|
settings.setPrintSides(PrintSides.TUMBLE);
|
||||||
|
}
|
||||||
|
if (pxlOpts.getPrinterTray() != null) {
|
||||||
|
PaperSource tray = findFXTray(fxPrinter.getPrinterAttributes().getSupportedPaperSources(), pxlOpts.getPrinterTray());
|
||||||
|
if (tray != null) {
|
||||||
|
settings.setPaperSource(tray);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pxlOpts.getDensity() > 0) {
|
||||||
|
settings.setPrintResolution(PrintHelper.createPrintResolution((int)pxlOpts.getDensity(), (int)pxlOpts.getDensity()));
|
||||||
|
}
|
||||||
|
|
||||||
|
Paper paper;
|
||||||
|
if (pxlOpts.getSize() != null && pxlOpts.getSize().getWidth() > 0 && pxlOpts.getSize().getHeight() > 0) {
|
||||||
|
double convert = 1;
|
||||||
|
Units units = getUnits(pxlOpts);
|
||||||
|
if (units == null) {
|
||||||
|
convert = 10; //need to adjust from cm to mm only for DPCM sizes
|
||||||
|
units = Units.MM;
|
||||||
|
}
|
||||||
|
paper = PrintHelper.createPaper("Custom", pxlOpts.getSize().getWidth() * convert, pxlOpts.getSize().getHeight() * convert, units);
|
||||||
|
} else {
|
||||||
|
PrintOptions.Size paperSize = options.getDefaultOptions().getPageSize();
|
||||||
|
paper = PrintHelper.createPaper("Default", paperSize.getWidth(), paperSize.getHeight(), Units.POINT);
|
||||||
|
}
|
||||||
|
|
||||||
|
PageOrientation orient = fxPrinter.getPrinterAttributes().getDefaultPageOrientation();
|
||||||
|
if (pxlOpts.getOrientation() != null) {
|
||||||
|
orient = getOrientation(pxlOpts);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
PageLayout layout;
|
||||||
|
PrintOptions.Margins m = pxlOpts.getMargins();
|
||||||
|
if (m != null) {
|
||||||
|
//force access to the page layout constructor as the adjusted margins on small sizes are wildly inaccurate
|
||||||
|
Constructor<PageLayout> plCon = PageLayout.class.getDeclaredConstructor(Paper.class, PageOrientation.class, double.class, double.class, double.class, double.class);
|
||||||
|
plCon.setAccessible(true);
|
||||||
|
|
||||||
|
//margins defined as pnt (1/72nds)
|
||||||
|
double asPnt = pxlOpts.getUnits().toInches() * 72;
|
||||||
|
if (orient == PageOrientation.PORTRAIT || orient == PageOrientation.REVERSE_PORTRAIT) {
|
||||||
|
layout = plCon.newInstance(paper, orient, m.left() * asPnt, m.right() * asPnt, m.top() * asPnt, m.bottom() * asPnt);
|
||||||
|
} else {
|
||||||
|
//rotate margins to match raster prints
|
||||||
|
layout = plCon.newInstance(paper, orient, m.top() * asPnt, m.bottom() * asPnt, m.right() * asPnt, m.left() * asPnt);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
//if margins are not provided, use default paper margins
|
||||||
|
PageLayout valid = fxPrinter.getDefaultPageLayout();
|
||||||
|
layout = fxPrinter.createPageLayout(paper, orient, valid.getLeftMargin(), valid.getRightMargin(), valid.getTopMargin(), valid.getBottomMargin());
|
||||||
|
}
|
||||||
|
|
||||||
|
//force our layout as the default to avoid default-margin exceptions on small paper sizes
|
||||||
|
Field field = fxPrinter.getClass().getDeclaredField("defPageLayout");
|
||||||
|
field.setAccessible(true);
|
||||||
|
field.set(fxPrinter, layout);
|
||||||
|
|
||||||
|
settings.setPageLayout(layout);
|
||||||
|
}
|
||||||
|
catch(Exception e) {
|
||||||
|
log.error("Failed to set custom layout", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.setCopies(pxlOpts.getCopies());
|
||||||
|
log.trace("{}", settings.toString());
|
||||||
|
|
||||||
|
//javaFX lies about this value, so pull from original print service
|
||||||
|
CopiesSupported cSupport = (CopiesSupported)output.getPrintService()
|
||||||
|
.getSupportedAttributeValues(Copies.class, output.getPrintService().getSupportedDocFlavors()[0], null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (cSupport != null && cSupport.contains(pxlOpts.getCopies())) {
|
||||||
|
for(WebAppModel model : models) {
|
||||||
|
WebApp.print(job, model);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
settings.setCopies(1); //manually handle copies if they are not supported
|
||||||
|
for(int i = 0; i < pxlOpts.getCopies(); i++) {
|
||||||
|
for(WebAppModel model : models) {
|
||||||
|
WebApp.print(job, model);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(Throwable t) {
|
||||||
|
job.cancelJob();
|
||||||
|
throw new PrinterException(t.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
//send pending prints
|
||||||
|
job.endJob();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void printLegacy(PrintOutput output, PrintOptions options) throws PrinterException {
|
||||||
|
PrintOptions.Pixel pxlOpts = options.getPixelOptions();
|
||||||
|
|
||||||
|
java.awt.print.PrinterJob job = java.awt.print.PrinterJob.getPrinterJob();
|
||||||
|
job.setPrintService(output.getPrintService());
|
||||||
|
PageFormat page = job.getPageFormat(null);
|
||||||
|
|
||||||
|
PrintRequestAttributeSet attributes = applyDefaultSettings(pxlOpts, page, output.getSupportedMedia());
|
||||||
|
|
||||||
|
//setup swing ui
|
||||||
|
JFrame legacyFrame = new JFrame(pxlOpts.getJobName(Constants.HTML_PRINT));
|
||||||
|
legacyFrame.setUndecorated(true);
|
||||||
|
legacyFrame.setLayout(new FlowLayout());
|
||||||
|
legacyFrame.setExtendedState(Frame.ICONIFIED);
|
||||||
|
|
||||||
|
legacyLabel = new JLabel();
|
||||||
|
legacyLabel.setOpaque(true);
|
||||||
|
legacyLabel.setBackground(Color.WHITE);
|
||||||
|
legacyLabel.setBorder(null);
|
||||||
|
legacyLabel.setDoubleBuffered(false);
|
||||||
|
|
||||||
|
legacyFrame.add(legacyLabel);
|
||||||
|
|
||||||
|
try {
|
||||||
|
for(WebAppModel model : models) {
|
||||||
|
if (model.isPlainText()) {
|
||||||
|
legacyLabel.setText(cleanHtmlContent(model.getSource()));
|
||||||
|
} else {
|
||||||
|
try(InputStream fis = new URL(model.getSource()).openStream()) {
|
||||||
|
String webPage = cleanHtmlContent(IOUtils.toString(fis, "UTF-8"));
|
||||||
|
legacyLabel.setText(webPage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
legacyFrame.pack();
|
||||||
|
legacyFrame.setVisible(true);
|
||||||
|
|
||||||
|
job.setPrintable(this);
|
||||||
|
printCopies(output, pxlOpts, job, attributes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(Exception e) {
|
||||||
|
throw new PrinterException(e.getMessage());
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
legacyFrame.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String cleanHtmlContent(String html) {
|
||||||
|
return html.replaceAll("^[\\s\\S]*<(HTML|html)\\b.*?>", "<html>");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int print(Graphics graphics, PageFormat pageFormat, int pageIndex) throws PrinterException {
|
||||||
|
if (legacyLabel == null) {
|
||||||
|
return super.print(graphics, pageFormat, pageIndex);
|
||||||
|
} else {
|
||||||
|
if (graphics == null) { throw new PrinterException("No graphics specified"); }
|
||||||
|
if (pageFormat == null) { throw new PrinterException("No page format specified"); }
|
||||||
|
|
||||||
|
if (pageIndex + 1 > models.size()) {
|
||||||
|
return NO_SUCH_PAGE;
|
||||||
|
}
|
||||||
|
log.trace("Requested page {} for printing", pageIndex);
|
||||||
|
|
||||||
|
Graphics2D graphics2D = (Graphics2D)graphics;
|
||||||
|
graphics2D.setRenderingHints(buildRenderingHints(dithering, interpolation));
|
||||||
|
graphics2D.translate(pageFormat.getImageableX(), pageFormat.getImageableY());
|
||||||
|
graphics2D.scale(pageFormat.getImageableWidth() / pageFormat.getWidth(), pageFormat.getImageableHeight() / pageFormat.getHeight());
|
||||||
|
legacyLabel.paint(graphics2D);
|
||||||
|
|
||||||
|
return PAGE_EXISTS;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void cleanup() {
|
||||||
|
super.cleanup();
|
||||||
|
|
||||||
|
models.clear();
|
||||||
|
legacyLabel = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Units getUnits(PrintOptions.Pixel opts) {
|
||||||
|
switch(opts.getUnits()) {
|
||||||
|
case INCH:
|
||||||
|
return Units.INCH;
|
||||||
|
case MM:
|
||||||
|
return Units.MM;
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PageOrientation getOrientation(PrintOptions.Pixel opts) {
|
||||||
|
switch(opts.getOrientation()) {
|
||||||
|
case LANDSCAPE:
|
||||||
|
return PageOrientation.LANDSCAPE;
|
||||||
|
case REVERSE_LANDSCAPE:
|
||||||
|
return PageOrientation.REVERSE_LANDSCAPE;
|
||||||
|
case REVERSE_PORTRAIT:
|
||||||
|
return PageOrientation.REVERSE_PORTRAIT;
|
||||||
|
default:
|
||||||
|
return PageOrientation.PORTRAIT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PrintColor getColor(PrintOptions.Pixel opts) {
|
||||||
|
switch(opts.getColorType()) {
|
||||||
|
case COLOR:
|
||||||
|
return PrintColor.COLOR;
|
||||||
|
default:
|
||||||
|
return PrintColor.MONOCHROME;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
341
old code/tray/src/qz/printer/action/PrintImage.java
Executable file
341
old code/tray/src/qz/printer/action/PrintImage.java
Executable file
@@ -0,0 +1,341 @@
|
|||||||
|
/**
|
||||||
|
* @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.printer.action;
|
||||||
|
|
||||||
|
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.common.Constants;
|
||||||
|
import qz.printer.PrintOptions;
|
||||||
|
import qz.printer.PrintOutput;
|
||||||
|
import qz.utils.ConnectionUtilities;
|
||||||
|
import qz.utils.PrintingUtilities;
|
||||||
|
import qz.utils.SystemUtilities;
|
||||||
|
|
||||||
|
import javax.imageio.IIOException;
|
||||||
|
import javax.imageio.ImageIO;
|
||||||
|
import javax.print.attribute.PrintRequestAttributeSet;
|
||||||
|
import javax.print.attribute.ResolutionSyntax;
|
||||||
|
import javax.print.attribute.standard.OrientationRequested;
|
||||||
|
import javax.print.attribute.standard.PrinterResolution;
|
||||||
|
import java.awt.*;
|
||||||
|
import java.awt.geom.Rectangle2D;
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
|
import java.awt.print.PageFormat;
|
||||||
|
import java.awt.print.Printable;
|
||||||
|
import java.awt.print.PrinterException;
|
||||||
|
import java.awt.print.PrinterJob;
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Tres Finocchiaro, Anton Mezerny
|
||||||
|
*/
|
||||||
|
public class PrintImage extends PrintPixel implements PrintProcessor, Printable {
|
||||||
|
|
||||||
|
private static final Logger log = LogManager.getLogger(PrintImage.class);
|
||||||
|
|
||||||
|
protected List<BufferedImage> images;
|
||||||
|
|
||||||
|
protected double dpiScale = 1;
|
||||||
|
protected boolean scaleImage = false;
|
||||||
|
protected Object dithering = RenderingHints.VALUE_DITHER_DEFAULT;
|
||||||
|
protected Object interpolation = RenderingHints.VALUE_INTERPOLATION_BICUBIC;
|
||||||
|
protected double imageRotation = 0;
|
||||||
|
protected boolean manualReverse = false;
|
||||||
|
|
||||||
|
public PrintImage() {
|
||||||
|
images = new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PrintingUtilities.Format getFormat() {
|
||||||
|
return PrintingUtilities.Format.IMAGE;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void parseData(JSONArray printData, PrintOptions options) throws JSONException, UnsupportedOperationException {
|
||||||
|
dpiScale = (options.getPixelOptions().getDensity() * options.getPixelOptions().getUnits().as1Inch()) / 72.0;
|
||||||
|
|
||||||
|
for(int i = 0; i < printData.length(); i++) {
|
||||||
|
JSONObject data = printData.getJSONObject(i);
|
||||||
|
|
||||||
|
PrintingUtilities.Flavor flavor = PrintingUtilities.Flavor.parse(data, PrintingUtilities.Flavor.FILE);
|
||||||
|
|
||||||
|
try {
|
||||||
|
BufferedImage bi;
|
||||||
|
switch(flavor) {
|
||||||
|
case PLAIN:
|
||||||
|
// There's really no such thing as a 'PLAIN' image, assume it's a URL
|
||||||
|
case FILE:
|
||||||
|
bi = ImageIO.read(ConnectionUtilities.getInputStream(data.getString("data"), true));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
bi = ImageIO.read(new ByteArrayInputStream(flavor.read(data.getString("data"))));
|
||||||
|
}
|
||||||
|
|
||||||
|
images.add(bi);
|
||||||
|
}
|
||||||
|
catch(IIOException e) {
|
||||||
|
if (e.getCause() != null && e.getCause() instanceof FileNotFoundException) {
|
||||||
|
throw new UnsupportedOperationException("Image file specified could not be found.", e);
|
||||||
|
} else {
|
||||||
|
throw new UnsupportedOperationException(String.format("Cannot parse (%s)%s as an image", flavor, data.getString("data")), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(IOException e) {
|
||||||
|
throw new UnsupportedOperationException(String.format("Cannot parse (%s)%s as an image: %s", flavor, data.getString("data"), e.getLocalizedMessage()), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug("Parsed {} images for printing", images.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<BufferedImage> breakupOverPages(BufferedImage img, PageFormat page, PrintRequestAttributeSet attributes) {
|
||||||
|
List<BufferedImage> splits = new ArrayList<>();
|
||||||
|
|
||||||
|
Rectangle printBounds = new Rectangle(0, 0, (int)page.getImageableWidth(), (int)page.getImageableHeight());
|
||||||
|
PrinterResolution res = (PrinterResolution)attributes.get(PrinterResolution.class);
|
||||||
|
float dpi = res.getFeedResolution(1) / (float)ResolutionSyntax.DPI;
|
||||||
|
float cdpi = res.getCrossFeedResolution(1) / (float)ResolutionSyntax.DPI;
|
||||||
|
|
||||||
|
//printing uses 72dpi, convert so we can check split size correctly
|
||||||
|
int useWidth = (int)((img.getWidth() / cdpi) * 72);
|
||||||
|
int useHeight = (int)((img.getHeight() / dpi) * 72);
|
||||||
|
|
||||||
|
int columnsNeed = (int)Math.ceil(useWidth / page.getImageableWidth());
|
||||||
|
int rowsNeed = (int)Math.ceil(useHeight / page.getImageableHeight());
|
||||||
|
|
||||||
|
if (columnsNeed == 1 && rowsNeed == 1) {
|
||||||
|
log.trace("Unscaled image does not need spit");
|
||||||
|
splits.add(img);
|
||||||
|
} else {
|
||||||
|
log.trace("Image to be printed across {} pages", columnsNeed * rowsNeed);
|
||||||
|
//allows us to split the image at the actual dpi instead of 72
|
||||||
|
float upscale = dpi / 72f;
|
||||||
|
float c_upscale = cdpi / 72f;
|
||||||
|
|
||||||
|
for(int row = 0; row < rowsNeed; row++) {
|
||||||
|
for(int col = 0; col < columnsNeed; col++) {
|
||||||
|
Rectangle clip = new Rectangle((col * (int)(printBounds.width * c_upscale)), (row * (int)(printBounds.height * upscale)),
|
||||||
|
(int)(printBounds.width * c_upscale), (int)(printBounds.height * upscale));
|
||||||
|
|
||||||
|
if (clip.x + clip.width > img.getWidth()) { clip.width = img.getWidth() - clip.x; }
|
||||||
|
if (clip.y + clip.height > img.getHeight()) { clip.height = img.getHeight() - clip.y; }
|
||||||
|
|
||||||
|
splits.add(img.getSubimage(clip.x, clip.y, clip.width, clip.height));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return splits;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void print(PrintOutput output, PrintOptions options) throws PrinterException {
|
||||||
|
if (images.isEmpty()) {
|
||||||
|
log.warn("Nothing to print");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
PrinterJob job = PrinterJob.getPrinterJob();
|
||||||
|
job.setPrintService(output.getPrintService());
|
||||||
|
PageFormat page = job.getPageFormat(null);
|
||||||
|
|
||||||
|
PrintOptions.Pixel pxlOpts = options.getPixelOptions();
|
||||||
|
PrintRequestAttributeSet attributes = applyDefaultSettings(pxlOpts, page, output.getSupportedMedia());
|
||||||
|
|
||||||
|
scaleImage = pxlOpts.isScaleContent();
|
||||||
|
dithering = pxlOpts.getDithering();
|
||||||
|
interpolation = pxlOpts.getInterpolation();
|
||||||
|
imageRotation = pxlOpts.getRotation();
|
||||||
|
|
||||||
|
//reverse fix for OSX
|
||||||
|
if (SystemUtilities.isMac() && pxlOpts.getOrientation() != null
|
||||||
|
&& pxlOpts.getOrientation().getAsOrientRequested() == OrientationRequested.REVERSE_LANDSCAPE) {
|
||||||
|
imageRotation += 180;
|
||||||
|
manualReverse = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!scaleImage) {
|
||||||
|
//breakup large images to print across pages as needed
|
||||||
|
List<BufferedImage> split = new ArrayList<>();
|
||||||
|
for(BufferedImage bi : images) {
|
||||||
|
split.addAll(breakupOverPages(bi, page, attributes));
|
||||||
|
}
|
||||||
|
images = split;
|
||||||
|
}
|
||||||
|
|
||||||
|
job.setJobName(pxlOpts.getJobName(Constants.IMAGE_PRINT));
|
||||||
|
job.setPrintable(this, job.validatePage(page));
|
||||||
|
|
||||||
|
printCopies(output, pxlOpts, job, attributes);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int print(Graphics graphics, PageFormat pageFormat, int pageIndex) throws PrinterException {
|
||||||
|
if (graphics == null) { throw new PrinterException("No graphics specified"); }
|
||||||
|
if (pageFormat == null) { throw new PrinterException("No page format specified"); }
|
||||||
|
|
||||||
|
if (pageIndex + 1 > images.size()) {
|
||||||
|
return NO_SUCH_PAGE;
|
||||||
|
}
|
||||||
|
log.trace("Requested page {} for printing", pageIndex);
|
||||||
|
|
||||||
|
if ("sun.print.PeekGraphics".equals(graphics.getClass().getCanonicalName())) {
|
||||||
|
//java uses class only to query if a page needs printed - save memory/time by short circuiting
|
||||||
|
return PAGE_EXISTS;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//allows pages view to rotate in different orientations
|
||||||
|
graphics.drawString(" ", 0, 0);
|
||||||
|
|
||||||
|
BufferedImage imgToPrint = fixColorModel(images.get(pageIndex));
|
||||||
|
if (imageRotation % 360 != 0) {
|
||||||
|
imgToPrint = rotate(imgToPrint, imageRotation);
|
||||||
|
}
|
||||||
|
|
||||||
|
// apply image scaling
|
||||||
|
double boundW = pageFormat.getImageableWidth();
|
||||||
|
double boundH = pageFormat.getImageableHeight();
|
||||||
|
|
||||||
|
double imgW = imgToPrint.getWidth() / dpiScale;
|
||||||
|
double imgH = imgToPrint.getHeight() / dpiScale;
|
||||||
|
|
||||||
|
if (scaleImage) {
|
||||||
|
imgToPrint = scale(imgToPrint, pageFormat);
|
||||||
|
|
||||||
|
// adjust dimensions to smallest edge, keeping size ratio
|
||||||
|
if (((float)imgToPrint.getWidth() / (float)imgToPrint.getHeight()) >= (boundW / boundH)) {
|
||||||
|
imgW = boundW;
|
||||||
|
imgH = (imgToPrint.getHeight() / (imgToPrint.getWidth() / boundW));
|
||||||
|
} else {
|
||||||
|
imgW = (imgToPrint.getWidth() / (imgToPrint.getHeight() / boundH));
|
||||||
|
imgH = boundH;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
double boundX = pageFormat.getImageableX();
|
||||||
|
double boundY = pageFormat.getImageableY();
|
||||||
|
|
||||||
|
log.debug("Paper area: {},{}:{},{}", (int)boundX, (int)boundY, (int)boundW, (int)boundH);
|
||||||
|
log.trace("Image size: {},{}", imgW, imgH);
|
||||||
|
|
||||||
|
// Now we perform our rendering
|
||||||
|
Graphics2D graphics2D = (Graphics2D)graphics;
|
||||||
|
graphics2D.setRenderingHints(buildRenderingHints(dithering, interpolation));
|
||||||
|
log.trace("{}", graphics2D.getRenderingHints());
|
||||||
|
|
||||||
|
log.debug("Memory: {}m/{}m", (Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()) / 1048576, Runtime.getRuntime().maxMemory() / 1048576);
|
||||||
|
|
||||||
|
if (!manualReverse) {
|
||||||
|
graphics2D.drawImage(imgToPrint, (int)boundX, (int)boundY, (int)(boundX + imgW), (int)(boundY + imgH),
|
||||||
|
0, 0, imgToPrint.getWidth(), imgToPrint.getHeight(), null);
|
||||||
|
} else {
|
||||||
|
graphics2D.drawImage(imgToPrint, (int)(boundW + boundX - imgW), (int)(boundH + boundY - imgH), (int)(boundW + boundX), (int)(boundH + boundY),
|
||||||
|
0, 0, imgToPrint.getWidth(), imgToPrint.getHeight(), null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Valid page
|
||||||
|
return PAGE_EXISTS;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param image
|
||||||
|
* @param pageFormat
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
private BufferedImage scale(BufferedImage image, PageFormat pageFormat) {
|
||||||
|
//scale up to print density (using less of a stretch if image is already larger than page)
|
||||||
|
double upScale = dpiScale * Math.min((pageFormat.getImageableWidth() / image.getWidth()), (pageFormat.getImageableHeight() / image.getHeight()));
|
||||||
|
if (upScale > dpiScale) { upScale = dpiScale; } else if (upScale < 1) { upScale = 1; }
|
||||||
|
|
||||||
|
if (upScale > 1) {
|
||||||
|
log.debug("Scaling image up by x{}", upScale);
|
||||||
|
|
||||||
|
BufferedImage scaled = new BufferedImage((int)(image.getWidth() * upScale), (int)(image.getHeight() * upScale), BufferedImage.TYPE_INT_ARGB);
|
||||||
|
Graphics2D g2d = scaled.createGraphics();
|
||||||
|
g2d.setRenderingHints(buildRenderingHints(dithering, interpolation));
|
||||||
|
g2d.drawImage(image, 0, 0, (int)(image.getWidth() * upScale), (int)(image.getHeight() * upScale), null);
|
||||||
|
g2d.dispose();
|
||||||
|
|
||||||
|
return scaled;
|
||||||
|
} else {
|
||||||
|
log.debug("No need to upscale image");
|
||||||
|
return image;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rotates {@code image} by the specified {@code angle}.
|
||||||
|
*
|
||||||
|
* @param image BufferedImage to rotate
|
||||||
|
* @param angle Rotation angle in degrees
|
||||||
|
* @return Rotated image data
|
||||||
|
*/
|
||||||
|
public static BufferedImage rotate(BufferedImage image, double angle, Object dithering, Object interpolation) {
|
||||||
|
double rads = Math.toRadians(angle);
|
||||||
|
double sin = Math.abs(Math.sin(rads)), cos = Math.abs(Math.cos(rads));
|
||||||
|
|
||||||
|
int sWidth = image.getWidth(), sHeight = image.getHeight();
|
||||||
|
int eWidth = (int)Math.floor((sWidth * cos) + (sHeight * sin)), eHeight = (int)Math.floor((sHeight * cos) + (sWidth * sin));
|
||||||
|
|
||||||
|
BufferedImage result;
|
||||||
|
if(!GraphicsEnvironment.isHeadless()) {
|
||||||
|
GraphicsConfiguration gc = GraphicsEnvironment.getLocalGraphicsEnvironment().getScreenDevices()[0].getDefaultConfiguration();
|
||||||
|
result = gc.createCompatibleImage(eWidth, eHeight, Transparency.TRANSLUCENT);
|
||||||
|
} else {
|
||||||
|
result = new BufferedImage(eWidth, eHeight, Transparency.TRANSLUCENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
Graphics2D g2d = result.createGraphics();
|
||||||
|
g2d.setRenderingHints(buildRenderingHints(dithering, interpolation));
|
||||||
|
g2d.translate((eWidth - sWidth) / 2, (eHeight - sHeight) / 2);
|
||||||
|
g2d.rotate(rads, sWidth / 2, sHeight / 2);
|
||||||
|
|
||||||
|
if (angle % 90 == 0 || interpolation == RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR) {
|
||||||
|
g2d.drawRenderedImage(image, null);
|
||||||
|
} else {
|
||||||
|
g2d.setPaint(new TexturePaint(image, new Rectangle2D.Float(0, 0, image.getWidth(), image.getHeight())));
|
||||||
|
g2d.fillRect(0, 0, image.getWidth(), image.getHeight());
|
||||||
|
}
|
||||||
|
|
||||||
|
g2d.dispose();
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private BufferedImage rotate(BufferedImage image, double angle) {
|
||||||
|
return rotate(image, angle, dithering, interpolation);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void cleanup() {
|
||||||
|
images.clear();
|
||||||
|
|
||||||
|
dpiScale = 1.0;
|
||||||
|
scaleImage = false;
|
||||||
|
imageRotation = 0;
|
||||||
|
dithering = RenderingHints.VALUE_DITHER_DEFAULT;
|
||||||
|
interpolation = RenderingHints.VALUE_INTERPOLATION_BICUBIC;
|
||||||
|
manualReverse = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
326
old code/tray/src/qz/printer/action/PrintPDF.java
Executable file
326
old code/tray/src/qz/printer/action/PrintPDF.java
Executable file
@@ -0,0 +1,326 @@
|
|||||||
|
package qz.printer.action;
|
||||||
|
|
||||||
|
import com.github.zafarkhaja.semver.Version;
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
import org.apache.pdfbox.io.IOUtils;
|
||||||
|
import org.apache.pdfbox.multipdf.Splitter;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDPage;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDPageContentStream;
|
||||||
|
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
||||||
|
import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject;
|
||||||
|
import org.apache.pdfbox.printing.Scaling;
|
||||||
|
import org.codehaus.jettison.json.JSONArray;
|
||||||
|
import org.codehaus.jettison.json.JSONException;
|
||||||
|
import org.codehaus.jettison.json.JSONObject;
|
||||||
|
import qz.common.Constants;
|
||||||
|
import qz.printer.PrintOptions;
|
||||||
|
import qz.printer.PrintOutput;
|
||||||
|
import qz.printer.action.pdf.BookBundle;
|
||||||
|
import qz.printer.action.pdf.PDFWrapper;
|
||||||
|
import qz.utils.ConnectionUtilities;
|
||||||
|
import qz.utils.PrintingUtilities;
|
||||||
|
import qz.utils.SystemUtilities;
|
||||||
|
|
||||||
|
import javax.print.attribute.PrintRequestAttributeSet;
|
||||||
|
import javax.print.attribute.standard.Media;
|
||||||
|
import javax.print.attribute.standard.MediaPrintableArea;
|
||||||
|
import java.awt.*;
|
||||||
|
import java.awt.geom.AffineTransform;
|
||||||
|
import java.awt.print.PageFormat;
|
||||||
|
import java.awt.print.Paper;
|
||||||
|
import java.awt.print.PrinterException;
|
||||||
|
import java.awt.print.PrinterJob;
|
||||||
|
import java.io.*;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.IntStream;
|
||||||
|
|
||||||
|
public class PrintPDF extends PrintPixel implements PrintProcessor {
|
||||||
|
|
||||||
|
private static final Logger log = LogManager.getLogger(PrintPDF.class);
|
||||||
|
|
||||||
|
private List<PDDocument> originals;
|
||||||
|
private List<PDDocument> printables;
|
||||||
|
private Splitter splitter = new Splitter();
|
||||||
|
|
||||||
|
private double docWidth = 0;
|
||||||
|
private double docHeight = 0;
|
||||||
|
private boolean ignoreTransparency = false;
|
||||||
|
private boolean altFontRendering = false;
|
||||||
|
|
||||||
|
|
||||||
|
public PrintPDF() {
|
||||||
|
originals = new ArrayList<>();
|
||||||
|
printables = new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PrintingUtilities.Format getFormat() {
|
||||||
|
return PrintingUtilities.Format.PDF;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void parseData(JSONArray printData, PrintOptions options) throws JSONException, UnsupportedOperationException {
|
||||||
|
PrintOptions.Pixel pxlOpts = options.getPixelOptions();
|
||||||
|
double convert = 72.0 / pxlOpts.getUnits().as1Inch();
|
||||||
|
|
||||||
|
for(int i = 0; i < printData.length(); i++) {
|
||||||
|
JSONObject data = printData.getJSONObject(i);
|
||||||
|
HashSet<Integer> pagesToPrint = new HashSet<>();
|
||||||
|
|
||||||
|
if (!data.isNull("options")) {
|
||||||
|
JSONObject dataOpt = data.getJSONObject("options");
|
||||||
|
|
||||||
|
if (!dataOpt.isNull("pageWidth") && dataOpt.optDouble("pageWidth") > 0) {
|
||||||
|
docWidth = dataOpt.optDouble("pageWidth") * convert;
|
||||||
|
}
|
||||||
|
if (!dataOpt.isNull("pageHeight") && dataOpt.optDouble("pageHeight") > 0) {
|
||||||
|
docHeight = dataOpt.optDouble("pageHeight") * convert;
|
||||||
|
}
|
||||||
|
|
||||||
|
ignoreTransparency = dataOpt.optBoolean("ignoreTransparency", false);
|
||||||
|
altFontRendering = dataOpt.optBoolean("altFontRendering", false);
|
||||||
|
|
||||||
|
if (!dataOpt.isNull("pageRanges")) {
|
||||||
|
String[] ranges = dataOpt.optString("pageRanges", "").split(",");
|
||||||
|
for(String range : ranges) {
|
||||||
|
range = range.trim();
|
||||||
|
if(range.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String[] period = range.split("-");
|
||||||
|
|
||||||
|
try {
|
||||||
|
int start = Integer.parseInt(period[0]);
|
||||||
|
pagesToPrint.add(start);
|
||||||
|
|
||||||
|
if (period.length > 1) {
|
||||||
|
int end = Integer.parseInt(period[period.length - 1]);
|
||||||
|
pagesToPrint.addAll(IntStream.rangeClosed(start, end).boxed().collect(Collectors.toSet()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(NumberFormatException nfe) {
|
||||||
|
log.warn("Unable to parse page range {}.", range);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PrintingUtilities.Flavor flavor = PrintingUtilities.Flavor.parse(data, PrintingUtilities.Flavor.FILE);
|
||||||
|
|
||||||
|
try {
|
||||||
|
PDDocument doc;
|
||||||
|
switch(flavor) {
|
||||||
|
case PLAIN:
|
||||||
|
// There's really no such thing as a 'PLAIN' PDF, assume it's a URL
|
||||||
|
case FILE:
|
||||||
|
doc = PDDocument.load(ConnectionUtilities.getInputStream(data.getString("data"), true));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
doc = PDDocument.load(new ByteArrayInputStream(flavor.read(data.getString("data"))));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pxlOpts.getBounds() != null) {
|
||||||
|
PrintOptions.Bounds bnd = pxlOpts.getBounds();
|
||||||
|
|
||||||
|
for(PDPage page : doc.getPages()) {
|
||||||
|
PDRectangle box = new PDRectangle(
|
||||||
|
(float)(bnd.getX() * convert),
|
||||||
|
page.getMediaBox().getUpperRightY() - (float)((bnd.getHeight() + bnd.getY()) * convert),
|
||||||
|
(float)(bnd.getWidth() * convert),
|
||||||
|
(float)(bnd.getHeight() * convert));
|
||||||
|
page.setMediaBox(box);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pagesToPrint.isEmpty()) {
|
||||||
|
pagesToPrint.addAll(IntStream.rangeClosed(1, doc.getNumberOfPages()).boxed().collect(Collectors.toSet()));
|
||||||
|
}
|
||||||
|
|
||||||
|
originals.add(doc);
|
||||||
|
|
||||||
|
List<PDDocument> splitPages = splitter.split(doc);
|
||||||
|
originals.addAll(splitPages); //ensures non-ranged page will still get closed
|
||||||
|
|
||||||
|
for(int pg = 0; pg < splitPages.size(); pg++) {
|
||||||
|
if (pagesToPrint.contains(pg + 1)) { //ranges are 1-indexed
|
||||||
|
printables.add(splitPages.get(pg));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(FileNotFoundException e) {
|
||||||
|
throw new UnsupportedOperationException("PDF file specified could not be found.", e);
|
||||||
|
}
|
||||||
|
catch(IOException e) {
|
||||||
|
throw new UnsupportedOperationException(String.format("Cannot parse (%s)%s as a PDF file: %s", flavor, data.getString("data"), e.getLocalizedMessage()), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug("Parsed {} files for printing", printables.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PrintRequestAttributeSet applyDefaultSettings(PrintOptions.Pixel pxlOpts, PageFormat page, Media[] supported) {
|
||||||
|
if (pxlOpts.getOrientation() != null) {
|
||||||
|
//page orient does not set properly on pdfs with orientation requested attribute
|
||||||
|
page.setOrientation(pxlOpts.getOrientation().getAsOrientFormat());
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.applyDefaultSettings(pxlOpts, page, supported);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void print(PrintOutput output, PrintOptions options) throws PrinterException {
|
||||||
|
if (printables.isEmpty()) {
|
||||||
|
log.warn("Nothing to print");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
PrinterJob job = PrinterJob.getPrinterJob();
|
||||||
|
job.setPrintService(output.getPrintService());
|
||||||
|
|
||||||
|
PrintOptions.Pixel pxlOpts = options.getPixelOptions();
|
||||||
|
Scaling scale = (pxlOpts.isScaleContent()? Scaling.SCALE_TO_FIT:Scaling.ACTUAL_SIZE);
|
||||||
|
|
||||||
|
PrintRequestAttributeSet attributes = applyDefaultSettings(pxlOpts, job.getPageFormat(null), (Media[])output.getPrintService().getSupportedAttributeValues(Media.class, null, null));
|
||||||
|
|
||||||
|
// Disable attributes per https://github.com/qzind/tray/issues/174
|
||||||
|
if (SystemUtilities.isMac() && Constants.JAVA_VERSION.compareWithBuildsTo(Version.valueOf("1.8.0+202")) < 0) {
|
||||||
|
log.warn("MacOS and Java < 1.8.0u202 cannot use attributes with PDF prints, disabling");
|
||||||
|
attributes.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
RenderingHints hints = new RenderingHints(buildRenderingHints(pxlOpts.getDithering(), pxlOpts.getInterpolation()));
|
||||||
|
double useDensity = pxlOpts.getDensity();
|
||||||
|
|
||||||
|
if (!pxlOpts.isRasterize()) {
|
||||||
|
if (pxlOpts.getDensity() > 0) {
|
||||||
|
// clear density for vector prints (applied via print attributes instead)
|
||||||
|
useDensity = 0;
|
||||||
|
} else if (SystemUtilities.isMac() && Constants.JAVA_VERSION.compareWithBuildsTo(Version.valueOf("1.8.0+121")) < 0) {
|
||||||
|
log.warn("OSX systems cannot print vector PDF's, forcing raster to prevent crash.");
|
||||||
|
useDensity = options.getDefaultOptions().getDensity();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BookBundle bundle = new BookBundle();
|
||||||
|
|
||||||
|
for(PDDocument doc : printables) {
|
||||||
|
PageFormat page = job.getPageFormat(null);
|
||||||
|
applyDefaultSettings(pxlOpts, page, output.getSupportedMedia());
|
||||||
|
|
||||||
|
//trick pdfbox into an alternate doc size if specified
|
||||||
|
if (docWidth > 0 || docHeight > 0) {
|
||||||
|
Paper paper = page.getPaper();
|
||||||
|
|
||||||
|
if (docWidth <= 0) { docWidth = page.getImageableWidth(); }
|
||||||
|
if (docHeight <= 0) { docHeight = page.getImageableHeight(); }
|
||||||
|
|
||||||
|
paper.setImageableArea(paper.getImageableX(), paper.getImageableY(), docWidth, docHeight);
|
||||||
|
page.setPaper(paper);
|
||||||
|
|
||||||
|
scale = Scaling.SCALE_TO_FIT; //to get custom size we need to force scaling
|
||||||
|
|
||||||
|
//pdf uses imageable area from Paper, so this can be safely removed
|
||||||
|
attributes.remove(MediaPrintableArea.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
for(PDPage pd : doc.getPages()) {
|
||||||
|
if (pxlOpts.getRotation() % 360 != 0) {
|
||||||
|
rotatePage(doc, pd, pxlOpts.getRotation());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pxlOpts.getOrientation() == null) {
|
||||||
|
PDRectangle bounds = pd.getBBox();
|
||||||
|
if ((page.getImageableHeight() > page.getImageableWidth() && bounds.getWidth() > bounds.getHeight()) ^ (pd.getRotation() / 90) % 2 == 1) {
|
||||||
|
log.info("Adjusting orientation to print landscape PDF source");
|
||||||
|
page.setOrientation(PrintOptions.Orientation.LANDSCAPE.getAsOrientFormat());
|
||||||
|
}
|
||||||
|
} else if (pxlOpts.getOrientation() != PrintOptions.Orientation.PORTRAIT) {
|
||||||
|
//flip imageable area dimensions when in landscape
|
||||||
|
Paper repap = page.getPaper();
|
||||||
|
repap.setImageableArea(repap.getImageableX(), repap.getImageableY(), repap.getImageableHeight(), repap.getImageableWidth());
|
||||||
|
page.setPaper(repap);
|
||||||
|
|
||||||
|
//reverse fix for OSX
|
||||||
|
if (SystemUtilities.isMac() && pxlOpts.getOrientation() == PrintOptions.Orientation.REVERSE_LANDSCAPE) {
|
||||||
|
pd.setRotation(pd.getRotation() + 180);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PDFWrapper wrapper = new PDFWrapper(doc, scale, false, ignoreTransparency, altFontRendering,
|
||||||
|
(float)(useDensity * pxlOpts.getUnits().as1Inch()),
|
||||||
|
false, pxlOpts.getOrientation(), hints);
|
||||||
|
|
||||||
|
bundle.append(wrapper, page, doc.getNumberOfPages());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pxlOpts.getSpoolSize() > 0 && bundle.getNumberOfPages() > pxlOpts.getSpoolSize()) {
|
||||||
|
int jobNum = 1;
|
||||||
|
int offset = 0;
|
||||||
|
while(offset < bundle.getNumberOfPages()) {
|
||||||
|
job.setJobName(pxlOpts.getJobName(Constants.PDF_PRINT) + "-" + jobNum++);
|
||||||
|
job.setPageable(bundle.wrapAndPresent(offset, pxlOpts.getSpoolSize()));
|
||||||
|
|
||||||
|
printCopies(output, pxlOpts, job, attributes);
|
||||||
|
|
||||||
|
offset += pxlOpts.getSpoolSize();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
job.setJobName(pxlOpts.getJobName(Constants.PDF_PRINT));
|
||||||
|
job.setPageable(bundle.wrapAndPresent());
|
||||||
|
|
||||||
|
printCopies(output, pxlOpts, job, attributes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void rotatePage(PDDocument doc, PDPage page, double rotation) {
|
||||||
|
try {
|
||||||
|
//copy page to object for manipulation
|
||||||
|
PDFormXObject xobject = new PDFormXObject(doc);
|
||||||
|
InputStream src = page.getContents();
|
||||||
|
OutputStream dest = xobject.getStream().createOutputStream();
|
||||||
|
|
||||||
|
try { IOUtils.copy(src, dest); }
|
||||||
|
finally {
|
||||||
|
IOUtils.closeQuietly(src);
|
||||||
|
IOUtils.closeQuietly(dest);
|
||||||
|
}
|
||||||
|
|
||||||
|
xobject.setResources(page.getResources());
|
||||||
|
xobject.setBBox(page.getBBox());
|
||||||
|
|
||||||
|
//draw our object at a rotated angle
|
||||||
|
AffineTransform transform = new AffineTransform();
|
||||||
|
transform.rotate(Math.toRadians(360 - rotation), xobject.getBBox().getWidth() / 2.0, xobject.getBBox().getHeight() / 2.0);
|
||||||
|
xobject.setMatrix(transform);
|
||||||
|
|
||||||
|
PDPageContentStream stream = new PDPageContentStream(doc, page);
|
||||||
|
stream.drawForm(xobject);
|
||||||
|
stream.close();
|
||||||
|
}
|
||||||
|
catch(IOException e) {
|
||||||
|
log.warn("Failed to rotate PDF page for printing");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void cleanup() {
|
||||||
|
for(PDDocument doc : originals) {
|
||||||
|
try { doc.close(); } catch(IOException ignore) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
originals.clear();
|
||||||
|
printables.clear();
|
||||||
|
|
||||||
|
docWidth = 0;
|
||||||
|
docHeight = 0;
|
||||||
|
ignoreTransparency = false;
|
||||||
|
altFontRendering = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
231
old code/tray/src/qz/printer/action/PrintPixel.java
Executable file
231
old code/tray/src/qz/printer/action/PrintPixel.java
Executable file
@@ -0,0 +1,231 @@
|
|||||||
|
package qz.printer.action;
|
||||||
|
|
||||||
|
import javafx.print.PaperSource;
|
||||||
|
import org.apache.commons.lang3.ArrayUtils;
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
import qz.printer.PrintOptions;
|
||||||
|
import qz.printer.PrintOutput;
|
||||||
|
import qz.utils.SystemUtilities;
|
||||||
|
|
||||||
|
import javax.print.attribute.HashPrintRequestAttributeSet;
|
||||||
|
import javax.print.attribute.PrintRequestAttributeSet;
|
||||||
|
import javax.print.attribute.standard.*;
|
||||||
|
import java.awt.*;
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
|
import java.awt.image.ColorModel;
|
||||||
|
import java.awt.image.IndexColorModel;
|
||||||
|
import java.awt.print.PageFormat;
|
||||||
|
import java.awt.print.Paper;
|
||||||
|
import java.awt.print.PrinterException;
|
||||||
|
import java.awt.print.PrinterJob;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
public abstract class PrintPixel {
|
||||||
|
|
||||||
|
private static final Logger log = LogManager.getLogger(PrintPixel.class);
|
||||||
|
|
||||||
|
private static final List<Integer> MAC_BAD_IMAGE_TYPES = Arrays.asList(BufferedImage.TYPE_BYTE_BINARY, BufferedImage.TYPE_CUSTOM);
|
||||||
|
|
||||||
|
|
||||||
|
protected PrintRequestAttributeSet applyDefaultSettings(PrintOptions.Pixel pxlOpts, PageFormat page, Media[] supported) {
|
||||||
|
PrintRequestAttributeSet attributes = new HashPrintRequestAttributeSet();
|
||||||
|
|
||||||
|
//apply general attributes
|
||||||
|
// If colortype is default, leave printColor blank. The system's printer settings will be used instead.
|
||||||
|
if (pxlOpts.getColorType() != PrintOptions.ColorType.DEFAULT) {
|
||||||
|
attributes.add(pxlOpts.getColorType().getAsChromaticity());
|
||||||
|
}
|
||||||
|
attributes.add(pxlOpts.getDuplex());
|
||||||
|
if (pxlOpts.getOrientation() != null) {
|
||||||
|
attributes.add(pxlOpts.getOrientation().getAsOrientRequested());
|
||||||
|
}
|
||||||
|
if (pxlOpts.getPrinterTray() != null && !pxlOpts.getPrinterTray().isEmpty()) {
|
||||||
|
Media tray = findMediaTray(supported, pxlOpts.getPrinterTray());
|
||||||
|
if (tray != null) {
|
||||||
|
attributes.add(tray);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO - set paper thickness
|
||||||
|
|
||||||
|
|
||||||
|
// Java prints using inches at 72dpi
|
||||||
|
final float CONVERT = pxlOpts.getUnits().toInches() * 72f;
|
||||||
|
|
||||||
|
log.trace("DPI: [{}x{}]\tCNV: {}", pxlOpts.getDensity(), pxlOpts.getCrossDensity(), CONVERT);
|
||||||
|
if (pxlOpts.getDensity() > 0) {
|
||||||
|
double cross = pxlOpts.getCrossDensity();
|
||||||
|
if (cross == 0) { cross = pxlOpts.getDensity(); }
|
||||||
|
|
||||||
|
attributes.add(new PrinterResolution((int)cross, (int)pxlOpts.getDensity(), pxlOpts.getUnits().getDPIUnits()));
|
||||||
|
}
|
||||||
|
|
||||||
|
//apply sizing and margins
|
||||||
|
Paper paper = page.getPaper();
|
||||||
|
|
||||||
|
float pageX = 0f;
|
||||||
|
float pageY = 0f;
|
||||||
|
float pageW = (float)page.getWidth() / CONVERT;
|
||||||
|
float pageH = (float)page.getHeight() / CONVERT;
|
||||||
|
|
||||||
|
//page size
|
||||||
|
if (pxlOpts.getSize() != null && pxlOpts.getSize().getWidth() > 0 && pxlOpts.getSize().getHeight() > 0) {
|
||||||
|
pageW = (float)pxlOpts.getSize().getWidth();
|
||||||
|
pageH = (float)pxlOpts.getSize().getHeight();
|
||||||
|
|
||||||
|
paper.setSize(pageW * CONVERT, pageH * CONVERT);
|
||||||
|
}
|
||||||
|
|
||||||
|
//margins
|
||||||
|
if (pxlOpts.getMargins() != null) {
|
||||||
|
pageX += pxlOpts.getMargins().left();
|
||||||
|
pageY += pxlOpts.getMargins().top();
|
||||||
|
pageW -= (pxlOpts.getMargins().right() + pxlOpts.getMargins().left());
|
||||||
|
pageH -= (pxlOpts.getMargins().bottom() + pxlOpts.getMargins().top());
|
||||||
|
}
|
||||||
|
|
||||||
|
log.trace("Drawable area: {},{}:{},{}", pageX, pageY, pageW, pageH);
|
||||||
|
if (pageW > 0 && pageH > 0) {
|
||||||
|
attributes.add(new MediaPrintableArea(pageX, pageY, pageW, pageH, pxlOpts.getUnits().getMediaSizeUnits()));
|
||||||
|
paper.setImageableArea(pageX * CONVERT, pageY * CONVERT, pageW * CONVERT, pageH * CONVERT);
|
||||||
|
page.setPaper(paper);
|
||||||
|
} else {
|
||||||
|
log.warn("Could not apply custom size, using printer default");
|
||||||
|
attributes.add(new MediaPrintableArea(0, 0, (float)page.getWidth() / 72f, (float)page.getHeight() / 72f, PrintOptions.Unit.INCH.getMediaSizeUnits()));
|
||||||
|
}
|
||||||
|
|
||||||
|
log.trace("{}", Arrays.toString(attributes.toArray()));
|
||||||
|
|
||||||
|
return attributes;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected void printCopies(PrintOutput output, PrintOptions.Pixel pxlOpts, PrinterJob job, PrintRequestAttributeSet attributes) throws PrinterException {
|
||||||
|
log.info("Starting printing ({} copies)", pxlOpts.getCopies());
|
||||||
|
|
||||||
|
PrinterResolution rUsing = (PrinterResolution)attributes.get(PrinterResolution.class);
|
||||||
|
if (rUsing != null) {
|
||||||
|
List<PrinterResolution> rSupport = output.getNativePrinter().getResolutions();
|
||||||
|
if (!rSupport.isEmpty()) {
|
||||||
|
if (!rSupport.contains(rUsing)) {
|
||||||
|
log.warn("Not using a supported DPI for printing");
|
||||||
|
log.debug("Available DPI: {}", ArrayUtils.toString(rSupport));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.warn("Supported printer densities not found");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CopiesSupported cSupport = (CopiesSupported)output.getPrintService()
|
||||||
|
.getSupportedAttributeValues(Copies.class, output.getPrintService().getSupportedDocFlavors()[0], attributes);
|
||||||
|
|
||||||
|
if (cSupport != null && cSupport.contains(pxlOpts.getCopies())) {
|
||||||
|
attributes.add(new Copies(pxlOpts.getCopies()));
|
||||||
|
job.print(attributes);
|
||||||
|
} else {
|
||||||
|
for(int i = 0; i < pxlOpts.getCopies(); i++) {
|
||||||
|
job.print(attributes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FIXME: Temporary fix for OS X 10.10 hard crash.
|
||||||
|
* See https://github.com/qzind/qz-print/issues/75
|
||||||
|
*/
|
||||||
|
protected BufferedImage fixColorModel(BufferedImage imgToPrint) {
|
||||||
|
if (SystemUtilities.isMac()) {
|
||||||
|
if (MAC_BAD_IMAGE_TYPES.contains(imgToPrint.getType())) {
|
||||||
|
BufferedImage sanitizedImage;
|
||||||
|
ColorModel cm = imgToPrint.getColorModel();
|
||||||
|
|
||||||
|
if (cm instanceof IndexColorModel) {
|
||||||
|
log.info("Image converted to 256 colors for OSX 10.10 Workaround");
|
||||||
|
sanitizedImage = new BufferedImage(imgToPrint.getWidth(), imgToPrint.getHeight(), BufferedImage.TYPE_BYTE_INDEXED, (IndexColorModel)cm);
|
||||||
|
} else {
|
||||||
|
log.info("Image converted to ARGB for OSX 10.10 Workaround");
|
||||||
|
sanitizedImage = new BufferedImage(imgToPrint.getWidth(), imgToPrint.getHeight(), BufferedImage.TYPE_INT_ARGB);
|
||||||
|
}
|
||||||
|
|
||||||
|
sanitizedImage.createGraphics().drawImage(imgToPrint, 0, 0, null);
|
||||||
|
imgToPrint = sanitizedImage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return imgToPrint;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static Map<RenderingHints.Key,Object> buildRenderingHints(Object dithering, Object interpolation) {
|
||||||
|
Map<RenderingHints.Key,Object> rhMap = new HashMap<>();
|
||||||
|
rhMap.put(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
|
||||||
|
rhMap.put(RenderingHints.KEY_DITHERING, dithering);
|
||||||
|
rhMap.put(RenderingHints.KEY_INTERPOLATION, interpolation);
|
||||||
|
rhMap.put(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY);
|
||||||
|
rhMap.put(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
|
||||||
|
rhMap.put(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE);
|
||||||
|
if (RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR.equals(interpolation)) {
|
||||||
|
rhMap.put(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_OFF);
|
||||||
|
rhMap.put(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF);
|
||||||
|
} else {
|
||||||
|
rhMap.put(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
|
||||||
|
rhMap.put(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return rhMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Media findMediaTray(Media[] supportedMedia, String traySelection) {
|
||||||
|
HashMap<String,Media> mediaTrays = new HashMap<>();
|
||||||
|
for(Media m : supportedMedia) {
|
||||||
|
if (m instanceof MediaTray) {
|
||||||
|
mediaTrays.put(m.toString(), m);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String tray = findTray(mediaTrays.keySet(), traySelection);
|
||||||
|
return mediaTrays.get(tray);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected PaperSource findFXTray(Set<PaperSource> paperSources, String traySelection) {
|
||||||
|
Map<String,PaperSource> fxTrays = paperSources.stream().collect(Collectors.toMap(PaperSource::getName, Function.identity()));
|
||||||
|
|
||||||
|
String tray = findTray(fxTrays.keySet(), traySelection);
|
||||||
|
return fxTrays.get(tray);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String findTray(Set<String> trayOptions, String traySelection) {
|
||||||
|
Pattern exactPattern = Pattern.compile("\\b" + Pattern.quote(traySelection) + "\\b", Pattern.CASE_INSENSITIVE);
|
||||||
|
Pattern fuzzyPattern = Pattern.compile("\\b.*?[" + Pattern.quote(traySelection) + "]+.*?\\b", Pattern.CASE_INSENSITIVE);
|
||||||
|
String bestFit = null;
|
||||||
|
Integer fuzzyFitDelta = null;
|
||||||
|
|
||||||
|
for(String option : trayOptions) {
|
||||||
|
Matcher exactly = exactPattern.matcher(option.trim());
|
||||||
|
Matcher fuzzily = fuzzyPattern.matcher(option.trim());
|
||||||
|
|
||||||
|
if (exactly.find()) {
|
||||||
|
bestFit = option;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
while(fuzzily.find()) {
|
||||||
|
//look for as close to exact match as possible
|
||||||
|
int delta = Math.abs(fuzzily.group().length() - traySelection.length());
|
||||||
|
if (fuzzyFitDelta == null || delta < fuzzyFitDelta) {
|
||||||
|
fuzzyFitDelta = delta;
|
||||||
|
bestFit = option;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return bestFit;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
39
old code/tray/src/qz/printer/action/PrintProcessor.java
Executable file
39
old code/tray/src/qz/printer/action/PrintProcessor.java
Executable file
@@ -0,0 +1,39 @@
|
|||||||
|
package qz.printer.action;
|
||||||
|
|
||||||
|
import org.codehaus.jettison.json.JSONArray;
|
||||||
|
import org.codehaus.jettison.json.JSONException;
|
||||||
|
import qz.printer.PrintOptions;
|
||||||
|
import qz.printer.PrintOutput;
|
||||||
|
import qz.utils.PrintingUtilities;
|
||||||
|
|
||||||
|
import javax.print.PrintException;
|
||||||
|
import java.awt.print.PrinterException;
|
||||||
|
|
||||||
|
public interface PrintProcessor {
|
||||||
|
|
||||||
|
|
||||||
|
PrintingUtilities.Format getFormat();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to parse information passed from the web API for printing.
|
||||||
|
*
|
||||||
|
* @param printData JSON Array of printer data
|
||||||
|
* @param options Printing options to use for the print job
|
||||||
|
*/
|
||||||
|
void parseData(JSONArray printData, PrintOptions options) throws JSONException, UnsupportedOperationException;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to setup and send documents to the specified printing {@code service}.
|
||||||
|
*
|
||||||
|
* @param output Destination used for printing
|
||||||
|
* @param options Printing options to use for the print job
|
||||||
|
*/
|
||||||
|
void print(PrintOutput output, PrintOptions options) throws PrintException, PrinterException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset a processor back to it's initial state.
|
||||||
|
*/
|
||||||
|
void cleanup();
|
||||||
|
|
||||||
|
}
|
||||||
530
old code/tray/src/qz/printer/action/PrintRaw.java
Executable file
530
old code/tray/src/qz/printer/action/PrintRaw.java
Executable file
@@ -0,0 +1,530 @@
|
|||||||
|
/**
|
||||||
|
* @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.printer.action;
|
||||||
|
|
||||||
|
import com.ibm.icu.text.ArabicShapingException;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||||
|
import org.apache.pdfbox.pdmodel.common.PDRectangle;
|
||||||
|
import org.apache.pdfbox.rendering.PDFRenderer;
|
||||||
|
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.common.ByteArrayBuilder;
|
||||||
|
import qz.common.Constants;
|
||||||
|
import qz.exception.NullCommandException;
|
||||||
|
import qz.exception.NullPrintServiceException;
|
||||||
|
import qz.printer.action.raw.ImageWrapper;
|
||||||
|
import qz.printer.action.raw.LanguageType;
|
||||||
|
import qz.printer.PrintOptions;
|
||||||
|
import qz.printer.PrintOutput;
|
||||||
|
import qz.printer.action.html.WebApp;
|
||||||
|
import qz.printer.action.html.WebAppModel;
|
||||||
|
import qz.printer.info.NativePrinter;
|
||||||
|
import qz.printer.status.CupsUtils;
|
||||||
|
import qz.utils.*;
|
||||||
|
|
||||||
|
import javax.imageio.ImageIO;
|
||||||
|
import javax.print.*;
|
||||||
|
import javax.print.attribute.HashPrintRequestAttributeSet;
|
||||||
|
import javax.print.attribute.PrintRequestAttributeSet;
|
||||||
|
import javax.print.attribute.standard.JobName;
|
||||||
|
import javax.print.event.PrintJobEvent;
|
||||||
|
import javax.print.event.PrintJobListener;
|
||||||
|
import java.awt.*;
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
|
import java.io.*;
|
||||||
|
import java.net.Socket;
|
||||||
|
import java.nio.charset.Charset;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends raw data to the printer, overriding your operating system's print
|
||||||
|
* driver. Most useful for printers such as zebra card or barcode printers.
|
||||||
|
*
|
||||||
|
* @author A. Tres Finocchiaro
|
||||||
|
*/
|
||||||
|
public class PrintRaw implements PrintProcessor {
|
||||||
|
|
||||||
|
private static final Logger log = LogManager.getLogger(PrintRaw.class);
|
||||||
|
|
||||||
|
private ByteArrayBuilder commands;
|
||||||
|
|
||||||
|
private String destEncoding = null;
|
||||||
|
|
||||||
|
private enum Backend {
|
||||||
|
CUPS_RSS,
|
||||||
|
CUPS_LPR,
|
||||||
|
WIN32_WMI
|
||||||
|
}
|
||||||
|
|
||||||
|
public PrintRaw() {
|
||||||
|
commands = new ByteArrayBuilder();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PrintingUtilities.Format getFormat() {
|
||||||
|
return PrintingUtilities.Format.COMMAND;
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] getBytes(String str, String destEncoding) throws ArabicShapingException, IOException {
|
||||||
|
switch(destEncoding.toLowerCase(Locale.ENGLISH)) {
|
||||||
|
case "ibm864":
|
||||||
|
case "cp864":
|
||||||
|
case "csibm864":
|
||||||
|
case "864":
|
||||||
|
case "ibm-864":
|
||||||
|
return ArabicConversionUtilities.convertToIBM864(str);
|
||||||
|
default:
|
||||||
|
return str.getBytes(destEncoding);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void parseData(JSONArray printData, PrintOptions options) throws JSONException, UnsupportedOperationException {
|
||||||
|
for(int i = 0; i < printData.length(); i++) {
|
||||||
|
JSONObject data = printData.optJSONObject(i);
|
||||||
|
if (data == null) {
|
||||||
|
data = new JSONObject();
|
||||||
|
data.put("data", printData.getString(i));
|
||||||
|
}
|
||||||
|
|
||||||
|
String cmd = data.getString("data");
|
||||||
|
JSONObject opt = data.optJSONObject("options");
|
||||||
|
if (opt == null) { opt = new JSONObject(); }
|
||||||
|
|
||||||
|
PrintingUtilities.Format format = PrintingUtilities.Format.valueOf(data.optString("format", "COMMAND").toUpperCase(Locale.ENGLISH));
|
||||||
|
PrintingUtilities.Flavor flavor = PrintingUtilities.Flavor.parse(data, PrintingUtilities.Flavor.PLAIN);
|
||||||
|
PrintOptions.Raw rawOpts = options.getRawOptions();
|
||||||
|
PrintOptions.Pixel pxlOpts = options.getPixelOptions();
|
||||||
|
|
||||||
|
destEncoding = rawOpts.getDestEncoding();
|
||||||
|
if (destEncoding == null || destEncoding.isEmpty()) { destEncoding = Charset.defaultCharset().name(); }
|
||||||
|
|
||||||
|
try {
|
||||||
|
switch(format) {
|
||||||
|
case HTML:
|
||||||
|
commands.append(getHtmlWrapper(cmd, opt, flavor, rawOpts, pxlOpts).getImageCommand(opt));
|
||||||
|
break;
|
||||||
|
case IMAGE:
|
||||||
|
commands.append(getImageWrapper(cmd, opt, flavor, rawOpts, pxlOpts).getImageCommand(opt));
|
||||||
|
break;
|
||||||
|
case PDF:
|
||||||
|
commands.append(getPdfWrapper(cmd, opt, flavor, rawOpts, pxlOpts).getImageCommand(opt));
|
||||||
|
break;
|
||||||
|
case COMMAND:
|
||||||
|
default:
|
||||||
|
switch(flavor) {
|
||||||
|
case PLAIN:
|
||||||
|
commands.append(getBytes(cmd, destEncoding));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
commands.append(seekConversion(flavor.read(cmd, opt.optString("xmlTag", null)), rawOpts));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(Exception e) {
|
||||||
|
throw new UnsupportedOperationException(String.format("Cannot parse (%s)%s into a raw %s command: %s", flavor, data.getString("data"), format, e.getLocalizedMessage()), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] seekConversion(byte[] rawBytes, PrintOptions.Raw rawOpts) {
|
||||||
|
if (rawOpts.getSrcEncoding() != null) {
|
||||||
|
if(rawOpts.getSrcEncoding().equals(rawOpts.getDestEncoding()) || rawOpts.getDestEncoding() == null) {
|
||||||
|
log.warn("Provided srcEncoding and destEncoding are the same, skipping");
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
String rawConvert = new String(rawBytes, rawOpts.getSrcEncoding());
|
||||||
|
return rawConvert.getBytes(rawOpts.getDestEncoding());
|
||||||
|
}
|
||||||
|
catch(UnsupportedEncodingException e) {
|
||||||
|
throw new UnsupportedOperationException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rawBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ImageWrapper getImageWrapper(String data, JSONObject opt, PrintingUtilities.Flavor flavor, PrintOptions.Raw rawOpts, PrintOptions.Pixel pxlOpts) throws IOException {
|
||||||
|
BufferedImage bi;
|
||||||
|
// 2.0 compat
|
||||||
|
if (data.startsWith("data:image/") && data.contains(";base64,")) {
|
||||||
|
String[] parts = data.split(";base64,");
|
||||||
|
data = parts[parts.length - 1];
|
||||||
|
flavor = PrintingUtilities.Flavor.BASE64;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch(flavor) {
|
||||||
|
case PLAIN:
|
||||||
|
// There's really no such thing as a 'PLAIN' image, assume it's a URL
|
||||||
|
case FILE:
|
||||||
|
bi = ImageIO.read(ConnectionUtilities.getInputStream(data, true));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
bi = ImageIO.read(new ByteArrayInputStream(seekConversion(flavor.read(data), rawOpts)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return getWrapper(bi, opt, pxlOpts);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ImageWrapper getPdfWrapper(String data, JSONObject opt, PrintingUtilities.Flavor flavor, PrintOptions.Raw rawOpts, PrintOptions.Pixel pxlOpts) throws IOException {
|
||||||
|
PDDocument doc;
|
||||||
|
|
||||||
|
switch(flavor) {
|
||||||
|
case PLAIN:
|
||||||
|
// There's really no such thing as a 'PLAIN' PDF, assume it's a URL
|
||||||
|
case FILE:
|
||||||
|
doc = PDDocument.load(ConnectionUtilities.getInputStream(data, true));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
doc = PDDocument.load(new ByteArrayInputStream(seekConversion(flavor.read(data), rawOpts)));
|
||||||
|
}
|
||||||
|
|
||||||
|
double scale;
|
||||||
|
PDRectangle rect = doc.getPage(0).getBBox();
|
||||||
|
double pw = opt.optDouble("pageWidth", 0), ph = opt.optDouble("pageHeight", 0);
|
||||||
|
if (ph <= 0 || (pw > 0 && (rect.getWidth() / rect.getHeight()) >= (pw / ph))) {
|
||||||
|
scale = pw / rect.getWidth();
|
||||||
|
} else {
|
||||||
|
scale = ph / rect.getHeight();
|
||||||
|
}
|
||||||
|
if (scale <= 0) { scale = 1.0; }
|
||||||
|
|
||||||
|
BufferedImage bi = new PDFRenderer(doc).renderImage(0, (float)scale);
|
||||||
|
return getWrapper(bi, opt, pxlOpts);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ImageWrapper getHtmlWrapper(String data, JSONObject opt, PrintingUtilities.Flavor flavor, PrintOptions.Raw rawOpts, PrintOptions.Pixel pxlOpts) throws IOException {
|
||||||
|
switch(flavor) {
|
||||||
|
case FILE:
|
||||||
|
case PLAIN:
|
||||||
|
// We'll toggle between 'plain' and 'file' when we construct WebAppModel
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
data = new String(seekConversion(flavor.read(data), rawOpts), destEncoding);
|
||||||
|
}
|
||||||
|
|
||||||
|
double density = (pxlOpts.getDensity() * pxlOpts.getUnits().as1Inch());
|
||||||
|
if (density <= 1) {
|
||||||
|
density = LanguageType.getType(opt.optString("language")).getDefaultDensity();
|
||||||
|
}
|
||||||
|
double pageZoom = density / 72.0;
|
||||||
|
|
||||||
|
double pageWidth = opt.optInt("pageWidth") / density * 72;
|
||||||
|
double pageHeight = opt.optInt("pageHeight") / density * 72;
|
||||||
|
|
||||||
|
BufferedImage bi;
|
||||||
|
WebAppModel model = new WebAppModel(data, (flavor != PrintingUtilities.Flavor.FILE), pageWidth, pageHeight, false, pageZoom);
|
||||||
|
|
||||||
|
try {
|
||||||
|
WebApp.initialize(); //starts if not already started
|
||||||
|
bi = WebApp.raster(model);
|
||||||
|
|
||||||
|
// down scale back from web density
|
||||||
|
double scaleFactor = opt.optDouble("pageWidth", 0) / bi.getWidth();
|
||||||
|
BufferedImage scaled = new BufferedImage((int)(bi.getWidth() * scaleFactor), (int)(bi.getHeight() * scaleFactor), BufferedImage.TYPE_INT_ARGB);
|
||||||
|
Graphics2D g2d = scaled.createGraphics();
|
||||||
|
g2d.drawImage(bi, 0, 0, (int)(bi.getWidth() * scaleFactor), (int)(bi.getHeight() * scaleFactor), null);
|
||||||
|
g2d.dispose();
|
||||||
|
bi = scaled;
|
||||||
|
}
|
||||||
|
catch(Throwable t) {
|
||||||
|
if (model.getZoom() > 1 && t instanceof IllegalArgumentException) {
|
||||||
|
//probably a unrecognized image loader error, try at default zoom
|
||||||
|
try {
|
||||||
|
log.warn("Capture failed with increased zoom, attempting with default value");
|
||||||
|
model.setZoom(1);
|
||||||
|
bi = WebApp.raster(model);
|
||||||
|
}
|
||||||
|
catch(Throwable tt) {
|
||||||
|
log.error("Failed to capture html raster");
|
||||||
|
throw new IOException(tt);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.error("Failed to capture html raster");
|
||||||
|
throw new IOException(t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return getWrapper(bi, opt, pxlOpts);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ImageWrapper getWrapper(BufferedImage img, JSONObject opt, PrintOptions.Pixel pxlOpts) throws IOException {
|
||||||
|
if(img == null) {
|
||||||
|
throw new IOException("Image provided is empty or null and cannot be converted.");
|
||||||
|
}
|
||||||
|
// Rotate image using orientation or rotation before sending to ImageWrapper
|
||||||
|
if (pxlOpts.getOrientation() != null && pxlOpts.getOrientation() != PrintOptions.Orientation.PORTRAIT) {
|
||||||
|
img = PrintImage.rotate(img, pxlOpts.getOrientation().getDegreesRot(), pxlOpts.getDithering(), pxlOpts.getInterpolation());
|
||||||
|
} else if (pxlOpts.getRotation() % 360 != 0) {
|
||||||
|
img = PrintImage.rotate(img, pxlOpts.getRotation(), pxlOpts.getDithering(), pxlOpts.getInterpolation());
|
||||||
|
}
|
||||||
|
|
||||||
|
ImageWrapper iw = new ImageWrapper(img, LanguageType.getType(opt.optString("language")));
|
||||||
|
iw.setCharset(Charset.forName(destEncoding));
|
||||||
|
|
||||||
|
//ESC/POS only
|
||||||
|
int density = opt.optInt("dotDensity", -1);
|
||||||
|
if (density == -1) {
|
||||||
|
String dStr = opt.optString("dotDensity", null);
|
||||||
|
if (dStr != null && !dStr.isEmpty()) {
|
||||||
|
switch(dStr.toLowerCase(Locale.ENGLISH)) {
|
||||||
|
case "single": density = 32; break;
|
||||||
|
case "double": density = 33; break;
|
||||||
|
case "triple": density = 39; break;
|
||||||
|
// negative: legacy mode
|
||||||
|
case "single-legacy": density = -32; break;
|
||||||
|
case "double-legacy": density = -33; break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
density = 32; //default
|
||||||
|
}
|
||||||
|
}
|
||||||
|
iw.setDotDensity(density);
|
||||||
|
|
||||||
|
//EPL only
|
||||||
|
iw.setxPos(opt.optInt("x", 0));
|
||||||
|
iw.setyPos(opt.optInt("y", 0));
|
||||||
|
|
||||||
|
// PGL only
|
||||||
|
iw.setLogoId(opt.optString("logoId", ""));
|
||||||
|
iw.setIgpDots(opt.optBoolean("igpDots", false));
|
||||||
|
|
||||||
|
return iw;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void print(PrintOutput output, PrintOptions options) throws PrintException {
|
||||||
|
PrintOptions.Raw rawOpts = options.getRawOptions();
|
||||||
|
|
||||||
|
List<ByteArrayBuilder> pages;
|
||||||
|
if (rawOpts.getSpoolSize() > 0 && rawOpts.getSpoolEnd() != null && !rawOpts.getSpoolEnd().isEmpty()) {
|
||||||
|
try {
|
||||||
|
pages = ByteUtilities.splitByteArray(commands.getByteArray(), rawOpts.getSpoolEnd().getBytes(destEncoding), rawOpts.getSpoolSize());
|
||||||
|
}
|
||||||
|
catch(UnsupportedEncodingException e) {
|
||||||
|
throw new PrintException(e);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
pages = new ArrayList<>();
|
||||||
|
pages.add(commands);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<File> tempFiles = null;
|
||||||
|
for(int i = 0; i < rawOpts.getCopies(); i++) {
|
||||||
|
for(int j = 0; j < pages.size(); j++) {
|
||||||
|
ByteArrayBuilder bab = pages.get(j);
|
||||||
|
try {
|
||||||
|
if (output.isSetHost()) {
|
||||||
|
printToHost(output.getHost(), output.getPort(), bab.getByteArray());
|
||||||
|
} else if (output.isSetFile()) {
|
||||||
|
printToFile(output.getFile(), bab.getByteArray(), true);
|
||||||
|
} else {
|
||||||
|
if (rawOpts.isForceRaw()) {
|
||||||
|
if(tempFiles == null) {
|
||||||
|
tempFiles = new ArrayList<>(pages.size());
|
||||||
|
}
|
||||||
|
File tempFile;
|
||||||
|
if(tempFiles.size() <= j) {
|
||||||
|
tempFile = File.createTempFile("qz_raw_", null);
|
||||||
|
tempFiles.add(j, tempFile);
|
||||||
|
printToFile(tempFile, bab.getByteArray(), false);
|
||||||
|
} else {
|
||||||
|
tempFile = tempFiles.get(j);
|
||||||
|
}
|
||||||
|
if(SystemUtilities.isWindows()) {
|
||||||
|
// Placeholder only; not yet supported
|
||||||
|
printToBackend(output.getNativePrinter(), tempFile, Backend.WIN32_WMI);
|
||||||
|
} else {
|
||||||
|
// Try CUPS backend first, fallback to LPR
|
||||||
|
printToBackend(output.getNativePrinter(), tempFile, Backend.CUPS_RSS, Backend.CUPS_LPR);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
printToPrinter(output.getPrintService(), bab.getByteArray(), rawOpts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(IOException e) {
|
||||||
|
cleanupTempFiles(rawOpts.isRetainTemp(), tempFiles);
|
||||||
|
throw new PrintException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cleanupTempFiles(rawOpts.isRetainTemp(), tempFiles);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void cleanupTempFiles(boolean retainTemp, List<File> tempFiles) {
|
||||||
|
if(tempFiles != null) {
|
||||||
|
if (!retainTemp) {
|
||||||
|
for(File tempFile : tempFiles) {
|
||||||
|
if(tempFile != null) {
|
||||||
|
if(!tempFile.delete()) {
|
||||||
|
tempFile.deleteOnExit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.warn("Temp file(s) retained: {}", Arrays.toString(tempFiles.toArray()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A brute-force, however surprisingly elegant way to send a file to a networked printer.
|
||||||
|
* <p/>
|
||||||
|
* Please note that this will completely bypass the Print Spooler,
|
||||||
|
* so the Operating System will have absolutely no printer information.
|
||||||
|
* This is printing "blind".
|
||||||
|
*/
|
||||||
|
private void printToHost(String host, int port, byte[] cmds) throws IOException {
|
||||||
|
log.debug("Printing to host {}:{}", host, port);
|
||||||
|
|
||||||
|
//throws any exception and auto-closes socket and stream
|
||||||
|
try(Socket socket = new Socket(host, port); DataOutputStream out = new DataOutputStream(socket.getOutputStream())) {
|
||||||
|
out.write(cmds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes the raw commands directly to a file.
|
||||||
|
*
|
||||||
|
* @param file File to be written
|
||||||
|
*/
|
||||||
|
private void printToFile(File file, byte[] cmds, boolean locationRestricted) throws IOException {
|
||||||
|
if(file == null) throw new IOException("No file specified");
|
||||||
|
|
||||||
|
if(locationRestricted && !PrefsSearch.getBoolean(ArgValue.SECURITY_PRINT_TOFILE)) {
|
||||||
|
log.error("Printing to file '{}' is not permitted. Configure property '{}' to modify this behavior.",
|
||||||
|
file, ArgValue.SECURITY_PRINT_TOFILE.getMatch());
|
||||||
|
throw new IOException(String.format("Printing to file '%s' is not permitted", file));
|
||||||
|
}
|
||||||
|
|
||||||
|
log.debug("Printing to file: {}", file.getName());
|
||||||
|
|
||||||
|
//throws any exception and auto-closes stream
|
||||||
|
try(OutputStream out = new FileOutputStream(file)) {
|
||||||
|
out.write(cmds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a {@code SimpleDoc} with the {@code commands} byte array.
|
||||||
|
*/
|
||||||
|
private void printToPrinter(PrintService service, byte[] cmds, PrintOptions.Raw rawOpts) throws PrintException {
|
||||||
|
if (service == null) { throw new NullPrintServiceException("Service cannot be null"); }
|
||||||
|
if (cmds == null || cmds.length == 0) { throw new NullCommandException("No commands found to send to the printer"); }
|
||||||
|
|
||||||
|
SimpleDoc doc = new SimpleDoc(cmds, DocFlavor.BYTE_ARRAY.AUTOSENSE, null);
|
||||||
|
|
||||||
|
PrintRequestAttributeSet attributes = new HashPrintRequestAttributeSet();
|
||||||
|
attributes.add(new JobName(rawOpts.getJobName(Constants.RAW_PRINT), Locale.getDefault()));
|
||||||
|
|
||||||
|
DocPrintJob printJob = service.createPrintJob();
|
||||||
|
|
||||||
|
waitForPrint(printJob, doc, attributes);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void waitForPrint(DocPrintJob printJob, Doc doc, PrintRequestAttributeSet attributes) throws PrintException {
|
||||||
|
final AtomicBoolean finished = new AtomicBoolean(false);
|
||||||
|
printJob.addPrintJobListener(new PrintJobListener() {
|
||||||
|
@Override
|
||||||
|
public void printDataTransferCompleted(PrintJobEvent printJobEvent) {
|
||||||
|
log.debug("{}", printJobEvent);
|
||||||
|
finished.set(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void printJobCompleted(PrintJobEvent printJobEvent) {
|
||||||
|
log.debug("{}", printJobEvent);
|
||||||
|
finished.set(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void printJobFailed(PrintJobEvent printJobEvent) {
|
||||||
|
log.error("{}", printJobEvent);
|
||||||
|
finished.set(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void printJobCanceled(PrintJobEvent printJobEvent) {
|
||||||
|
log.warn("{}", printJobEvent);
|
||||||
|
finished.set(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void printJobNoMoreEvents(PrintJobEvent printJobEvent) {
|
||||||
|
log.debug("{}", printJobEvent);
|
||||||
|
finished.set(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void printJobRequiresAttention(PrintJobEvent printJobEvent) {
|
||||||
|
log.info("{}", printJobEvent);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
log.trace("Sending print job to printer");
|
||||||
|
printJob.print(doc, attributes);
|
||||||
|
|
||||||
|
while(!finished.get()) {
|
||||||
|
try { Thread.sleep(100); } catch(Exception ignore) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.trace("Print job received by printer");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Direct/backend printing modes for forced raw printing
|
||||||
|
*/
|
||||||
|
public void printToBackend(NativePrinter printer, File tempFile, Backend... backends) throws IOException, PrintException {
|
||||||
|
boolean success = false;
|
||||||
|
|
||||||
|
for(Backend backend : backends) {
|
||||||
|
switch(backend) {
|
||||||
|
case CUPS_LPR:
|
||||||
|
// Use command line "lp" on Linux, BSD, Solaris, OSX, etc.
|
||||||
|
String[] lpCmd = new String[] {"lp", "-d", printer.getPrinterId(), "-o", "raw", tempFile.getAbsolutePath()};
|
||||||
|
if (!(success = ShellUtilities.execute(lpCmd))) {
|
||||||
|
log.debug(StringUtils.join(lpCmd, ' '));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case CUPS_RSS:
|
||||||
|
// Submit job via cupsDoRequest(...) via JNA against localhost:631\
|
||||||
|
success = CupsUtils.sendRawFile(printer, tempFile);
|
||||||
|
break;
|
||||||
|
case WIN32_WMI:
|
||||||
|
default:
|
||||||
|
throw new UnsupportedOperationException("Raw backend \"" + backend + "\" is not yet supported.");
|
||||||
|
}
|
||||||
|
if(success) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!success) {
|
||||||
|
throw new PrintException("Forced raw printing failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void cleanup() {
|
||||||
|
commands.clear();
|
||||||
|
destEncoding = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
44
old code/tray/src/qz/printer/action/ProcessorFactory.java
Executable file
44
old code/tray/src/qz/printer/action/ProcessorFactory.java
Executable file
@@ -0,0 +1,44 @@
|
|||||||
|
package qz.printer.action;
|
||||||
|
|
||||||
|
import org.apache.commons.pool2.KeyedPooledObjectFactory;
|
||||||
|
import org.apache.commons.pool2.PooledObject;
|
||||||
|
import org.apache.commons.pool2.impl.DefaultPooledObject;
|
||||||
|
import qz.utils.PrintingUtilities;
|
||||||
|
|
||||||
|
public class ProcessorFactory implements KeyedPooledObjectFactory<PrintingUtilities.Format,PrintProcessor> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PooledObject<PrintProcessor> makeObject(PrintingUtilities.Format key) throws Exception {
|
||||||
|
PrintProcessor processor;
|
||||||
|
switch(key) {
|
||||||
|
case HTML: processor = new PrintHTML(); break;
|
||||||
|
case IMAGE: processor = new PrintImage(); break;
|
||||||
|
case PDF: processor = new PrintPDF(); break;
|
||||||
|
case DIRECT: processor = new PrintDirect(); break;
|
||||||
|
case COMMAND: default: processor = new PrintRaw(); break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new DefaultPooledObject<>(processor);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean validateObject(PrintingUtilities.Format key, PooledObject<PrintProcessor> p) {
|
||||||
|
return true; //no-op
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void activateObject(PrintingUtilities.Format key, PooledObject<PrintProcessor> p) throws Exception {
|
||||||
|
//no-op
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void passivateObject(PrintingUtilities.Format key, PooledObject<PrintProcessor> p) throws Exception {
|
||||||
|
p.getObject().cleanup();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void destroyObject(PrintingUtilities.Format key, PooledObject<PrintProcessor> p) throws Exception {
|
||||||
|
//no-op
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
512
old code/tray/src/qz/printer/action/html/WebApp.java
Executable file
512
old code/tray/src/qz/printer/action/html/WebApp.java
Executable file
@@ -0,0 +1,512 @@
|
|||||||
|
package qz.printer.action.html;
|
||||||
|
|
||||||
|
import com.github.zafarkhaja.semver.Version;
|
||||||
|
import com.sun.javafx.tk.TKPulseListener;
|
||||||
|
import com.sun.javafx.tk.Toolkit;
|
||||||
|
import javafx.animation.AnimationTimer;
|
||||||
|
import javafx.application.Application;
|
||||||
|
import javafx.application.Platform;
|
||||||
|
import javafx.beans.value.ChangeListener;
|
||||||
|
import javafx.concurrent.Worker;
|
||||||
|
import javafx.embed.swing.SwingFXUtils;
|
||||||
|
import javafx.print.PageLayout;
|
||||||
|
import javafx.print.PrinterJob;
|
||||||
|
import javafx.scene.Scene;
|
||||||
|
import javafx.scene.shape.Rectangle;
|
||||||
|
import javafx.scene.transform.Scale;
|
||||||
|
import javafx.scene.transform.Transform;
|
||||||
|
import javafx.scene.transform.Translate;
|
||||||
|
import javafx.scene.web.WebView;
|
||||||
|
import javafx.stage.Stage;
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
import org.w3c.dom.Attr;
|
||||||
|
import org.w3c.dom.Document;
|
||||||
|
import org.w3c.dom.Node;
|
||||||
|
import org.w3c.dom.NodeList;
|
||||||
|
import qz.common.Constants;
|
||||||
|
import qz.utils.SystemUtilities;
|
||||||
|
import qz.ws.PrintSocketServer;
|
||||||
|
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
import java.util.function.IntPredicate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JavaFX container for taking HTML snapshots.
|
||||||
|
* Used by PrintHTML to generate printable images.
|
||||||
|
* <p/>
|
||||||
|
* Do not use constructor (used by JavaFX), instead call {@code WebApp.initialize()}
|
||||||
|
*/
|
||||||
|
public class WebApp extends Application {
|
||||||
|
|
||||||
|
private static final Logger log = LogManager.getLogger(WebApp.class);
|
||||||
|
|
||||||
|
private static WebApp instance = null;
|
||||||
|
private static Version webkitVersion = null;
|
||||||
|
private static int CAPTURE_FRAMES = 2;
|
||||||
|
private static int VECTOR_FRAMES = 1;
|
||||||
|
private static Stage stage;
|
||||||
|
private static WebView webView;
|
||||||
|
private static double pageWidth;
|
||||||
|
private static double pageHeight;
|
||||||
|
private static double pageZoom;
|
||||||
|
private static boolean raster;
|
||||||
|
private static boolean headless;
|
||||||
|
|
||||||
|
private static CountDownLatch startupLatch;
|
||||||
|
private static CountDownLatch captureLatch;
|
||||||
|
|
||||||
|
private static IntPredicate printAction;
|
||||||
|
private static final AtomicReference<Throwable> thrown = new AtomicReference<>();
|
||||||
|
|
||||||
|
// JDK-8283686: Printing WebView may results in empty page
|
||||||
|
private static final Version JDK_8283686_START = Version.valueOf(/* WebKit */ "609.1.0");
|
||||||
|
private static final Version JDK_8283686_END = Version.valueOf(/* WebKit */ "612.1.0");
|
||||||
|
private static final int JDK_8283686_VECTOR_FRAMES = 30;
|
||||||
|
|
||||||
|
|
||||||
|
//listens for a Succeeded state to activate image capture
|
||||||
|
private static ChangeListener<Worker.State> stateListener = (ov, oldState, newState) -> {
|
||||||
|
log.trace("New state: {} > {}", oldState, newState);
|
||||||
|
|
||||||
|
// Cancelled should probably throw exception listener, but does not
|
||||||
|
if (newState == Worker.State.CANCELLED) {
|
||||||
|
// This can happen for file downloads, e.g. "response-content-disposition=attachment"
|
||||||
|
// See https://github.com/qzind/tray/issues/1183
|
||||||
|
unlatch(new IOException("Page load was cancelled for an unknown reason"));
|
||||||
|
}
|
||||||
|
if (newState == Worker.State.SUCCEEDED) {
|
||||||
|
boolean hasBody = (boolean)webView.getEngine().executeScript("document.body != null");
|
||||||
|
if (!hasBody) {
|
||||||
|
log.warn("Loaded page has no body - likely a redirect, skipping state");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//ensure html tag doesn't use scrollbars, clipping page instead
|
||||||
|
Document doc = webView.getEngine().getDocument();
|
||||||
|
NodeList tags = doc.getElementsByTagName("html");
|
||||||
|
if (tags != null && tags.getLength() > 0) {
|
||||||
|
Node base = tags.item(0);
|
||||||
|
Attr applied = (Attr)base.getAttributes().getNamedItem("style");
|
||||||
|
if (applied == null) {
|
||||||
|
applied = doc.createAttribute("style");
|
||||||
|
}
|
||||||
|
applied.setValue(applied.getValue() + "; overflow: hidden;");
|
||||||
|
base.getAttributes().setNamedItem(applied);
|
||||||
|
}
|
||||||
|
|
||||||
|
//width was resized earlier (for responsive html), then calculate the best fit height
|
||||||
|
// FIXME: Should only be needed when height is unknown but fixes blank vector prints
|
||||||
|
double fittedHeight = findHeight();
|
||||||
|
boolean heightNeeded = pageHeight <= 0;
|
||||||
|
|
||||||
|
if (heightNeeded) {
|
||||||
|
pageHeight = fittedHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
// find and set page zoom for increased quality
|
||||||
|
double usableZoom = calculateSupportedZoom(pageWidth, pageHeight);
|
||||||
|
if (usableZoom < pageZoom) {
|
||||||
|
log.warn("Zoom level {} decreased to {} due to physical memory limitations", pageZoom, usableZoom);
|
||||||
|
pageZoom = usableZoom;
|
||||||
|
}
|
||||||
|
webView.setZoom(pageZoom);
|
||||||
|
log.trace("Zooming in by x{} for increased quality", pageZoom);
|
||||||
|
|
||||||
|
adjustSize(pageWidth * pageZoom, pageHeight * pageZoom);
|
||||||
|
|
||||||
|
//need to check for height again as resizing can cause partial results
|
||||||
|
if (heightNeeded) {
|
||||||
|
fittedHeight = findHeight();
|
||||||
|
if (fittedHeight != pageHeight) {
|
||||||
|
adjustSize(pageWidth * pageZoom, fittedHeight * pageZoom);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.trace("Set HTML page height to {}", pageHeight);
|
||||||
|
|
||||||
|
autosize(webView);
|
||||||
|
|
||||||
|
Platform.runLater(() -> new AnimationTimer() {
|
||||||
|
int frames = 0;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handle(long l) {
|
||||||
|
if (printAction.test(++frames)) {
|
||||||
|
stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.start());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
//listens for load progress
|
||||||
|
private static ChangeListener<Number> workDoneListener = (ov, oldWork, newWork) -> log.trace("Done: {} > {}", oldWork, newWork);
|
||||||
|
|
||||||
|
private static ChangeListener<String> msgListener = (ov, oldMsg, newMsg) -> log.trace("New status: {}", newMsg);
|
||||||
|
|
||||||
|
//listens for failures
|
||||||
|
private static ChangeListener<Throwable> exceptListener = (obs, oldExc, newExc) -> {
|
||||||
|
if (newExc != null) { unlatch(newExc); }
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/** Called by JavaFX thread */
|
||||||
|
public WebApp() {
|
||||||
|
instance = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Starts JavaFX thread if not already running */
|
||||||
|
public static synchronized void initialize() throws IOException {
|
||||||
|
if (instance == null) {
|
||||||
|
startupLatch = new CountDownLatch(1);
|
||||||
|
// For JDK8 compat
|
||||||
|
headless = false;
|
||||||
|
|
||||||
|
// JDK11+ depends bundled javafx
|
||||||
|
if (Constants.JAVA_VERSION.getMajorVersion() >= 11) {
|
||||||
|
// Monocle default for unit tests
|
||||||
|
boolean useMonocle = true;
|
||||||
|
if (PrintSocketServer.getTrayManager() != null) {
|
||||||
|
// Honor user monocle override
|
||||||
|
useMonocle = PrintSocketServer.getTrayManager().isMonoclePreferred();
|
||||||
|
// Trust TrayManager's headless detection
|
||||||
|
headless = PrintSocketServer.getTrayManager().isHeadless();
|
||||||
|
} else {
|
||||||
|
// Fallback for JDK11+
|
||||||
|
headless = true;
|
||||||
|
}
|
||||||
|
if (useMonocle && SystemUtilities.hasMonocle()) {
|
||||||
|
log.trace("Initializing monocle platform");
|
||||||
|
System.setProperty("javafx.platform", "monocle");
|
||||||
|
// Don't set glass.platform on Linux per https://github.com/qzind/tray/issues/702
|
||||||
|
switch(SystemUtilities.getOs()) {
|
||||||
|
case WINDOWS:
|
||||||
|
case MAC:
|
||||||
|
System.setProperty("glass.platform", "Monocle");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// don't set "glass.platform"
|
||||||
|
}
|
||||||
|
|
||||||
|
//software rendering required headless environments
|
||||||
|
if (headless) {
|
||||||
|
System.setProperty("prism.order", "sw");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.warn("Monocle platform will not be used");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
new Thread(() -> Application.launch(WebApp.class)).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startupLatch.getCount() > 0) {
|
||||||
|
try {
|
||||||
|
log.trace("Waiting for JavaFX..");
|
||||||
|
if (!startupLatch.await(60, TimeUnit.SECONDS)) {
|
||||||
|
throw new IOException("JavaFX did not start");
|
||||||
|
} else {
|
||||||
|
log.trace("Running a test snapshot to size the stage...");
|
||||||
|
try {
|
||||||
|
raster(new WebAppModel("<h1>startup</h1>", true, 0, 0, true, 2));
|
||||||
|
log.trace("JFX initialized successfully");
|
||||||
|
}
|
||||||
|
catch(Throwable t) {
|
||||||
|
throw new IOException(t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(InterruptedException ignore) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void start(Stage st) throws Exception {
|
||||||
|
startupLatch.countDown();
|
||||||
|
log.debug("Started JavaFX");
|
||||||
|
|
||||||
|
webView = new WebView();
|
||||||
|
|
||||||
|
// JDK-8283686: Printing WebView may results in empty page
|
||||||
|
// See also https://github.com/qzind/tray/issues/778
|
||||||
|
if(getWebkitVersion() == null ||
|
||||||
|
(getWebkitVersion().greaterThan(JDK_8283686_START) &&
|
||||||
|
getWebkitVersion().lessThan(JDK_8283686_END))) {
|
||||||
|
VECTOR_FRAMES = JDK_8283686_VECTOR_FRAMES; // Additional pulses needed for vector graphics
|
||||||
|
}
|
||||||
|
|
||||||
|
st.setScene(new Scene(webView));
|
||||||
|
stage = st;
|
||||||
|
stage.setWidth(1);
|
||||||
|
stage.setHeight(1);
|
||||||
|
|
||||||
|
Worker<Void> worker = webView.getEngine().getLoadWorker();
|
||||||
|
worker.stateProperty().addListener(stateListener);
|
||||||
|
worker.workDoneProperty().addListener(workDoneListener);
|
||||||
|
worker.exceptionProperty().addListener(exceptListener);
|
||||||
|
worker.messageProperty().addListener(msgListener);
|
||||||
|
|
||||||
|
//prevents JavaFX from shutting down when hiding window
|
||||||
|
Platform.setImplicitExit(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prints the loaded source specified in the passed {@code model}.
|
||||||
|
*
|
||||||
|
* @param job A setup JavaFx {@code PrinterJob}
|
||||||
|
* @param model The model specifying the web page parameters
|
||||||
|
* @throws Throwable JavaFx will throw a generic {@code Throwable} class for any issues
|
||||||
|
*/
|
||||||
|
public static synchronized void print(final PrinterJob job, final WebAppModel model) throws Throwable {
|
||||||
|
model.setZoom(1); //vector prints do not need to use zoom
|
||||||
|
raster = false;
|
||||||
|
|
||||||
|
load(model, (int frames) -> {
|
||||||
|
if(frames == VECTOR_FRAMES) {
|
||||||
|
try {
|
||||||
|
double printScale = 72d / 96d;
|
||||||
|
webView.getTransforms().add(new Scale(printScale, printScale));
|
||||||
|
|
||||||
|
PageLayout layout = job.getJobSettings().getPageLayout();
|
||||||
|
if (model.isScaled()) {
|
||||||
|
double viewWidth = webView.getWidth() * printScale;
|
||||||
|
double viewHeight = webView.getHeight() * printScale;
|
||||||
|
|
||||||
|
double scale;
|
||||||
|
if ((viewWidth / viewHeight) >= (layout.getPrintableWidth() / layout.getPrintableHeight())) {
|
||||||
|
scale = (layout.getPrintableWidth() / viewWidth);
|
||||||
|
} else {
|
||||||
|
scale = (layout.getPrintableHeight() / viewHeight);
|
||||||
|
}
|
||||||
|
webView.getTransforms().add(new Scale(scale, scale));
|
||||||
|
}
|
||||||
|
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
double useScale = 1;
|
||||||
|
for(Transform t : webView.getTransforms()) {
|
||||||
|
if (t instanceof Scale) { useScale *= ((Scale)t).getX(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
PageLayout page = job.getJobSettings().getPageLayout();
|
||||||
|
Rectangle printBounds = new Rectangle(0, 0, page.getPrintableWidth(), page.getPrintableHeight());
|
||||||
|
log.debug("Paper area: {},{}:{},{}", (int)page.getLeftMargin(), (int)page.getTopMargin(),
|
||||||
|
(int)page.getPrintableWidth(), (int)page.getPrintableHeight());
|
||||||
|
|
||||||
|
Translate activePage = new Translate();
|
||||||
|
webView.getTransforms().add(activePage);
|
||||||
|
|
||||||
|
int columnsNeed = Math.max(1, (int)Math.ceil(webView.getWidth() / printBounds.getWidth() * useScale - 0.1));
|
||||||
|
int rowsNeed = Math.max(1, (int)Math.ceil(webView.getHeight() / printBounds.getHeight() * useScale - 0.1));
|
||||||
|
log.debug("Document will be printed across {} pages", columnsNeed * rowsNeed);
|
||||||
|
|
||||||
|
try {
|
||||||
|
for(int row = 0; row < rowsNeed; row++) {
|
||||||
|
for(int col = 0; col < columnsNeed; col++) {
|
||||||
|
activePage.setX((-col * printBounds.getWidth()) / useScale);
|
||||||
|
activePage.setY((-row * printBounds.getHeight()) / useScale);
|
||||||
|
|
||||||
|
job.printPage(webView);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unlatch(null);
|
||||||
|
}
|
||||||
|
catch(Exception e) {
|
||||||
|
unlatch(e);
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
//reset state
|
||||||
|
webView.getTransforms().clear();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch(Exception e) { unlatch(e); }
|
||||||
|
}
|
||||||
|
return frames >= VECTOR_FRAMES;
|
||||||
|
});
|
||||||
|
|
||||||
|
log.trace("Waiting on print..");
|
||||||
|
captureLatch.await(); //released when unlatch is called
|
||||||
|
|
||||||
|
if (thrown.get() != null) { throw thrown.get(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static synchronized BufferedImage raster(final WebAppModel model) throws Throwable {
|
||||||
|
AtomicReference<BufferedImage> capture = new AtomicReference<>();
|
||||||
|
|
||||||
|
//ensure JavaFX has started before we run
|
||||||
|
if (startupLatch.getCount() > 0) {
|
||||||
|
throw new IOException("JavaFX has not been started");
|
||||||
|
}
|
||||||
|
|
||||||
|
//raster still needs to show stage for valid capture
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
stage.show();
|
||||||
|
stage.toBack();
|
||||||
|
});
|
||||||
|
|
||||||
|
raster = true;
|
||||||
|
|
||||||
|
load(model, (int frames) -> {
|
||||||
|
if (frames == CAPTURE_FRAMES) {
|
||||||
|
log.debug("Attempting image capture");
|
||||||
|
|
||||||
|
Toolkit.getToolkit().addPostSceneTkPulseListener(new TKPulseListener() {
|
||||||
|
@Override
|
||||||
|
public void pulse() {
|
||||||
|
try {
|
||||||
|
// TODO: Revert to Callback once JDK-8244588/SUPQZ-5 is avail (JDK11+ only)
|
||||||
|
capture.set(SwingFXUtils.fromFXImage(webView.snapshot(null, null), null));
|
||||||
|
unlatch(null);
|
||||||
|
}
|
||||||
|
catch(Exception e) {
|
||||||
|
unlatch(e);
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
Toolkit.getToolkit().removePostSceneTkPulseListener(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Toolkit.getToolkit().requestNextPulse();
|
||||||
|
}
|
||||||
|
|
||||||
|
return frames >= CAPTURE_FRAMES;
|
||||||
|
});
|
||||||
|
|
||||||
|
log.trace("Waiting on capture..");
|
||||||
|
captureLatch.await(); //released when unlatch is called
|
||||||
|
|
||||||
|
if (thrown.get() != null) { throw thrown.get(); }
|
||||||
|
|
||||||
|
return capture.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prints the loaded source specified in the passed {@code model}.
|
||||||
|
*
|
||||||
|
* @param model The model specifying the web page parameters.
|
||||||
|
* @param action EventHandler that will be ran when the WebView completes loading.
|
||||||
|
*/
|
||||||
|
private static synchronized void load(WebAppModel model, IntPredicate action) {
|
||||||
|
captureLatch = new CountDownLatch(1);
|
||||||
|
thrown.set(null);
|
||||||
|
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
//zoom should only be factored on raster prints
|
||||||
|
pageZoom = model.getZoom();
|
||||||
|
pageWidth = model.getWebWidth();
|
||||||
|
pageHeight = model.getWebHeight();
|
||||||
|
|
||||||
|
log.trace("Setting starting size {}:{}", pageWidth, pageHeight);
|
||||||
|
adjustSize(pageWidth * pageZoom, pageHeight * pageZoom);
|
||||||
|
|
||||||
|
if (pageHeight == 0) {
|
||||||
|
webView.setMinHeight(1);
|
||||||
|
webView.setPrefHeight(1);
|
||||||
|
webView.setMaxHeight(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
autosize(webView);
|
||||||
|
|
||||||
|
printAction = action;
|
||||||
|
|
||||||
|
if (model.isPlainText()) {
|
||||||
|
webView.getEngine().loadContent(model.getSource(), "text/html");
|
||||||
|
} else {
|
||||||
|
webView.getEngine().load(model.getSource());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double findHeight() {
|
||||||
|
String heightText = webView.getEngine().executeScript("Math.max(document.body.offsetHeight, document.body.scrollHeight)").toString();
|
||||||
|
return Double.parseDouble(heightText);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void adjustSize(double toWidth, double toHeight) {
|
||||||
|
webView.setMinSize(toWidth, toHeight);
|
||||||
|
webView.setPrefSize(toWidth, toHeight);
|
||||||
|
webView.setMaxSize(toWidth, toHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fix blank page after autosize is called
|
||||||
|
*/
|
||||||
|
public static void autosize(WebView webView) {
|
||||||
|
webView.autosize();
|
||||||
|
|
||||||
|
if (!raster) {
|
||||||
|
// Call updatePeer; fixes a bug with webView resizing
|
||||||
|
// Can be avoided by calling stage.show() but breaks headless environments
|
||||||
|
// See: https://github.com/qzind/tray/issues/513
|
||||||
|
String[] methods = {"impl_updatePeer" /*jfx8*/, "doUpdatePeer" /*jfx11*/};
|
||||||
|
try {
|
||||||
|
for(Method m : webView.getClass().getDeclaredMethods()) {
|
||||||
|
for(String method : methods) {
|
||||||
|
if (m.getName().equals(method)) {
|
||||||
|
m.setAccessible(true);
|
||||||
|
m.invoke(webView);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(SecurityException | ReflectiveOperationException e) {
|
||||||
|
log.warn("Unable to update peer; Blank pages may occur.", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double calculateSupportedZoom(double width, double height) {
|
||||||
|
long memory = Runtime.getRuntime().maxMemory();
|
||||||
|
int allowance = (memory / 1048576L) > 1024? 3:2;
|
||||||
|
if (headless) { allowance--; }
|
||||||
|
long availSpace = memory << allowance;
|
||||||
|
|
||||||
|
// Memory needed for print is roughly estimated as
|
||||||
|
// (width * height) [pixels needed] * (pageZoom * 72d) [print density used] * 3 [rgb channels]
|
||||||
|
return Math.sqrt(availSpace / ((width * height) * (pageZoom * 72d) * 3));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Final cleanup when no longer capturing
|
||||||
|
*/
|
||||||
|
public static void unlatch(Throwable t) {
|
||||||
|
if (t != null) {
|
||||||
|
thrown.set(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
captureLatch.countDown();
|
||||||
|
stage.hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Version getWebkitVersion() {
|
||||||
|
if(webkitVersion == null) {
|
||||||
|
if(webView != null) {
|
||||||
|
String userAgent = webView.getEngine().getUserAgent();
|
||||||
|
String[] parts = userAgent.split("WebKit/");
|
||||||
|
if (parts.length > 1) {
|
||||||
|
String[] split = parts[1].split(" ");
|
||||||
|
if (split.length > 0) {
|
||||||
|
try {
|
||||||
|
webkitVersion = Version.valueOf(split[0]);
|
||||||
|
log.info("WebKit version {} detected", webkitVersion);
|
||||||
|
} catch(Exception ignore) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(webkitVersion == null) {
|
||||||
|
log.warn("WebKit version couldn't be parsed from UserAgent: {}", userAgent);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.warn("Can't get WebKit version, JavaFX hasn't started yet.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return webkitVersion;
|
||||||
|
}
|
||||||
|
}
|
||||||
81
old code/tray/src/qz/printer/action/html/WebAppModel.java
Executable file
81
old code/tray/src/qz/printer/action/html/WebAppModel.java
Executable file
@@ -0,0 +1,81 @@
|
|||||||
|
package qz.printer.action.html;
|
||||||
|
|
||||||
|
public class WebAppModel {
|
||||||
|
|
||||||
|
private String source;
|
||||||
|
private boolean plainText;
|
||||||
|
|
||||||
|
private double width, webWidth;
|
||||||
|
private double height, webHeight;
|
||||||
|
private boolean isScaled;
|
||||||
|
private double zoom;
|
||||||
|
|
||||||
|
public WebAppModel(String source, boolean plainText, double width, double height, boolean isScaled, double zoom) {
|
||||||
|
this.source = source;
|
||||||
|
this.plainText = plainText;
|
||||||
|
this.width = width;
|
||||||
|
this.height = height;
|
||||||
|
this.webWidth = width * (96d / 72d);
|
||||||
|
this.webHeight = height * (96d / 72d);
|
||||||
|
this.isScaled = isScaled;
|
||||||
|
this.zoom = zoom;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getSource() {
|
||||||
|
return source;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSource(String source) {
|
||||||
|
this.source = source;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isPlainText() {
|
||||||
|
return plainText;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPlainText(boolean plainText) {
|
||||||
|
this.plainText = plainText;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getWidth() {
|
||||||
|
return width;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setWidth(double width) {
|
||||||
|
this.width = width;
|
||||||
|
this.webWidth = width * (96d / 72d);
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getHeight() {
|
||||||
|
return height;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setHeight(double height) {
|
||||||
|
this.height = height;
|
||||||
|
this.webHeight = height * (96d / 72d);
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getWebWidth() {
|
||||||
|
return webWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getWebHeight() {
|
||||||
|
return webHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isScaled() {
|
||||||
|
return isScaled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setScaled(boolean scaled) {
|
||||||
|
isScaled = scaled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public double getZoom() {
|
||||||
|
return zoom;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setZoom(double zoom) {
|
||||||
|
this.zoom = zoom;
|
||||||
|
}
|
||||||
|
}
|
||||||
80
old code/tray/src/qz/printer/action/pdf/BookBundle.java
Executable file
80
old code/tray/src/qz/printer/action/pdf/BookBundle.java
Executable file
@@ -0,0 +1,80 @@
|
|||||||
|
package qz.printer.action.pdf;
|
||||||
|
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
|
||||||
|
import java.awt.*;
|
||||||
|
import java.awt.print.Book;
|
||||||
|
import java.awt.print.PageFormat;
|
||||||
|
import java.awt.print.Printable;
|
||||||
|
import java.awt.print.PrinterException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapper of the {@code Book} class as a {@code Printable} type,
|
||||||
|
* since PrinterJob implementations do not seem to handle the {@code Pageable} interface properly.
|
||||||
|
*/
|
||||||
|
public class BookBundle extends Book {
|
||||||
|
|
||||||
|
private static final Logger log = LogManager.getLogger(BookBundle.class);
|
||||||
|
|
||||||
|
private Printable lastPrint;
|
||||||
|
private int lastStarted;
|
||||||
|
|
||||||
|
public BookBundle() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapper of the wrapper class so that PrinterJob implementations will handle it as proper pageable
|
||||||
|
*/
|
||||||
|
public Book wrapAndPresent() {
|
||||||
|
Book cover = new Book();
|
||||||
|
for(int i = 0; i < getNumberOfPages(); i++) {
|
||||||
|
cover.append(new PrintingPress(), getPageFormat(i));
|
||||||
|
}
|
||||||
|
|
||||||
|
return cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Book wrapAndPresent(int offset, int length) {
|
||||||
|
Book coverSubset = new Book();
|
||||||
|
for(int i = offset; i < offset + length && i < getNumberOfPages(); i++) {
|
||||||
|
coverSubset.append(new PrintingPress(offset), getPageFormat(i));
|
||||||
|
}
|
||||||
|
|
||||||
|
return coverSubset;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** Printable wrapper to ensure proper reading of multiple documents across spooling */
|
||||||
|
private class PrintingPress implements Printable {
|
||||||
|
private int pageOffset;
|
||||||
|
|
||||||
|
public PrintingPress() {
|
||||||
|
this(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public PrintingPress(int offset) {
|
||||||
|
pageOffset = offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int print(Graphics g, PageFormat format, int pageIndex) throws PrinterException {
|
||||||
|
pageIndex += pageOffset;
|
||||||
|
log.trace("Requested page {} for printing", pageIndex);
|
||||||
|
|
||||||
|
if (pageIndex < getNumberOfPages()) {
|
||||||
|
Printable printable = getPrintable(pageIndex);
|
||||||
|
if (printable != lastPrint) {
|
||||||
|
lastPrint = printable;
|
||||||
|
lastStarted = pageIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
return printable.print(g, format, pageIndex - lastStarted);
|
||||||
|
}
|
||||||
|
|
||||||
|
return NO_SUCH_PAGE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user