diff --git a/src/main/java/common/Activation.java b/src/main/java/common/Activation.java new file mode 100644 index 0000000..7d9702c --- /dev/null +++ b/src/main/java/common/Activation.java @@ -0,0 +1,656 @@ +package common; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathExpressionException; +import javax.xml.xpath.XPathFactory; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Reads, writes and maintains data contained in the activation.xml file, or alternative file + */ +public class Activation +{ + public static final String HOBBES_DEFAULT_VERSION = "10.0.4"; + + /** + * Enumeration for the type of operating system we're running on. + */ + public enum OS { WINDOWS, LINUX, MAC, UNKNOWN } + + private final Logger LOGGER = Logger.getLogger( Activation.class.getName() ); + public HttpUtils httpUtils; + protected Path activationFile; + protected boolean hasActivationToken = false; + + // properties stored in the activationServiceInfo, credentials and activationToken nodes. + protected HashMap properties = new HashMap<>(); + Set operatorURLList = new LinkedHashSet<>(); + Map licenseServiceCertificates = new HashMap<>(); + + // XPath factory and XPath instance for parsing + private final XPathFactory xpathFactory = XPathFactory.newInstance(); + private final XPath xpath = xpathFactory.newXPath(); + + /** + * Constructor which loads an existing activation file. + * + * @param activationFilePath path to the activation.xml file + * @param logLevel the initial logging level for this class + **/ + public Activation( Path activationFilePath, HttpUtils httpUtils, Level logLevel ) + { + LOGGER.setLevel( logLevel ); + this.activationFile = activationFilePath; + this.httpUtils = httpUtils; + + // Try to parse existing activation file, don't throw an exception on failure + if (null != activationFilePath && Files.exists( activationFilePath )) + { + try + { + parseActivationFile( false ); + } + catch (GourouException ignore){} + } + } + + /** + * Static factory method to create a Activation instance, possibly initializing data + * from a file. If the properties cannot be initialized from the file, this + * will contact the Adobe server for initialization and authorization info. + * If initialization from the network occurs, the activation file will be + * written to the file system. + * + * @param activationFilePath The path for activation file. This needs to be the full file + * path in case the name of the activation file was changed by the command line, + * @param ACSServer The ACS server URL (used for default activation file creation if needed). + * @return A new or existing Activation instance. + *

+ * TODO: should this be replaced with a constructor? + */ + public static Activation createActivation( Path activationFilePath, String ACSServer, HttpUtils httpUtils, Level logLevel ) + throws IOException, GourouException, ParserConfigurationException, SAXException + { + // start by ensuring the directory path exists; probably unnecessary?? + Path dirPath = activationFilePath.getParent(); + + if (!Files.exists( dirPath )) + { + Files.createDirectories( dirPath ); + } + + // This constructor parses the activation file, if it exists. No exception is thrown + // on parsing failure. No attempt is made to initialize the activation via the internet. + Activation activation = new Activation( activationFilePath, httpUtils, logLevel ); + + // if parsing failed, checkActivationInfo might access the Adobe server + boolean doUpdate = activation.checkActivationInfo( ACSServer ); + + if (activation.checkAuthCert()) + { + doUpdate = true; + } + + if (doUpdate) + { + activation.updateActivationFile(); + } + return activation; + } + + /** + * Retrieves an indicator of the operating system we're running on. + * + * @return an Activation.OS enumeration + */ + public static OS getOS() + { + String osName = System.getProperty( "os.name" ); + if (osName.toLowerCase().contains( "windows" )) + { + return OS.WINDOWS; + } + else if (osName.toLowerCase().contains( "linux" )) + { + return OS.LINUX; + } + else if (osName.toLowerCase().contains( "mac os x" ) || osName.toLowerCase().contains( "macos" )) + { + return OS.MAC; + } + else + { + return OS.UNKNOWN; + } + } + + /** + * Determines the default Adept directory. + * + * @return The default Adept directory path. + */ + public static String getDefaultAdeptDir() + { + String homeDir = System.getProperty( "user.home" ); + if (homeDir != null && !homeDir.isEmpty()) + { + // Using Paths.get for platform-independent path construction + if (getOS() == OS.MAC) + { + return Paths.get( homeDir, "/Library/Application Support/Adobe/Digital Editions").toString(); + } + return Paths.get( homeDir, ".config", "jadept" ).toString(); + } + else + { + // Fallback for environments without user.home, similar to LOCAL_ADEPT_DIR + Logger logger = Logger.getLogger( Activation.class.getName() ); + logger.log( Level.WARNING, "user.home not found, using local adept directory." ); + return "."; + } + } + + /** + * Parse the "credentials" DOM element and store the properties + * + * @param credentialsEl The DOM Element containing 5 or 6 properties + */ + public void storeCredentials( Element credentialsEl ) + { + parseActivationInfo( credentialsEl ); + // Handle username and login method from credentials + NodeList nameNodes = credentialsEl.getElementsByTagName( "username" ); + Element usernameNode = (Element) nameNodes.item( 0 ); + if (usernameNode != null) + { + String method = usernameNode.getAttribute( "method" ); + properties.put( "method", method ); + } + } + + /** + * parse the activationToken DOM Element and store the enclosed 6 properties + * + * @param activationEl see description + */ + public void updateActivationTokenData( Element activationEl ) + { + hasActivationToken = parseActivationInfo( activationEl ); + } + + public void setLogLevel( Level newLevel ) + { + LOGGER.setLevel( newLevel ); + } + + public boolean parseActivationInfo( Node activationInfo ) + { + NodeList nodes = activationInfo.getChildNodes(); + for (int i = 0; i < nodes.getLength(); i++) + { + Node node = nodes.item( i ); + if (node instanceof Element) + { // store the key values without prefixes + String tag = node.getLocalName(); + if (null == tag) + { + tag = ((Element) node).getTagName(); + } + properties.put( tag, node.getTextContent() ); + } + } + return true; + } + + protected Document parseActivationDocument( Document activationDoc, boolean throwOnFailure ) + throws XPathExpressionException + { + Element activationInfo = activationDoc.getDocumentElement(); + if (!activationInfo.getTagName().contains( "activationInfo" )) + { + // if the document node is not "activationInfo", find that node in the document. + // should never happen. + activationInfo = (Element) xpath + .evaluate( "activationInfo", + activationDoc.getDocumentElement(), XPathConstants.NODE ); + } + activationInfo.normalize(); + Node node = XmlUtils.getNode( activationDoc, "//adept:activationServiceInfo", false ); + parseActivationInfo( node ); // This method will populate the userName property, but not the method attribute + + xpath.setNamespaceContext( new XmlUtils.AdeptNamespaceContext( activationDoc ) ); + + // Extract credential elements using XPath + node = XmlUtils.getNode( activationDoc, "//adept:credentials", false ); + if (null != node) + { + parseActivationInfo( node ); + + // Handle username and login method from credentials + Element usernameNode = (Element) xpath.evaluate( "//adept:username", activationDoc, XPathConstants.NODE ); + if (usernameNode != null) + { + String method = usernameNode.getAttribute( "method" ); + properties.put( "method", method ); + } + else if (throwOnFailure) + { + throw new GourouException( "Invalid activation file: adept:username node not found." ); + } + + + if ("anonymous".equals( getLoginMethod() )) + { + properties.put( "username", "anonymous" ); + } + } + // Extract activationToken data using XPath + node = XmlUtils.getNode( activationDoc, "//adept:activationToken", false ); + if (null != node) + { + hasActivationToken = parseActivationInfo( node ); + } + // Populate licenseServiceCertificates map + NodeList licenseServiceNodes = (NodeList) xpath.evaluate( + "//adept:licenseServices/adept:licenseServiceInfo", activationDoc, XPathConstants.NODESET ); + for (int i = 0; i < licenseServiceNodes.getLength(); i++) + { + Element serviceInfoNode = (Element) licenseServiceNodes.item( i ); + String url = XmlUtils.getChildElementText( serviceInfoNode, "adept:licenseURL", false ); + String certificate = XmlUtils.getChildElementText( serviceInfoNode, "adept:certificate", false ); + if (!url.isEmpty()) + { + licenseServiceCertificates.put( url.trim(), certificate ); + } + } + node = XmlUtils.getNode( activationDoc, "//adept:operatorURLList", false ); + if (null != node) + { + // We don't save the user id, because it is just a repetition + licenseServiceNodes = (NodeList) xpath.evaluate( "//adept:operatorURL", activationDoc, + XPathConstants.NODESET ); + for (int i = 0; i < licenseServiceNodes.getLength(); i++) + { + Element serviceInfoNode = (Element) licenseServiceNodes.item( i ); + String url = serviceInfoNode.getTextContent(); + if (!url.isEmpty()) + { + operatorURLList.add( url.trim() ); + } + } + } + return activationDoc; + } + + /** + * Parses the activation.xml file and populates user properties + * + * @param throwOnFailure if false, an exception will not be thrown if parsing fails. + * @return the parsed activation XML Document + * @throws GourouException only when throwOnFailure is true. + */ + @SuppressWarnings("UnusedReturnValue") + protected Document parseActivationFile( boolean throwOnFailure ) + throws GourouException + { + LOGGER.log( Level.FINE, "DEBUG: Parse activation file " + activationFile ); + + DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance(); + docFactory.setNamespaceAware( true ); // Important for XPath with namespaces + DocumentBuilder docBuilder; + Document activationDoc = null; + try + { + docBuilder = docFactory.newDocumentBuilder(); + activationDoc = docBuilder.parse( activationFile.toFile() ); + return parseActivationDocument( activationDoc, throwOnFailure ); + } + catch (ParserConfigurationException e) + { + if (throwOnFailure) + { + throw new GourouException( "XML Parser configuration error: " + e.getMessage() ); + } + return null; + } + catch (SAXException | IOException | XPathExpressionException e) + { + if (throwOnFailure) + { + throw new GourouException( "Invalid activation file: " + e.getMessage() ); + } + } + return activationDoc; + } + + /* ** Getters and setters ** */ + public Path getActivationDirPath() + { + if (null != activationFile) + { + return activationFile.getParent(); + } + return Paths.get( "" ).toAbsolutePath(); + } + + //-- ActivationNode --// + public String getActivationURL() + { + return properties.get( "activationURL" ); + } + + public String getAuthURL() + { + return properties.getOrDefault( "authURL", "https://adeactivate.adobe.com/adept" ); + } + + public String getUserInfoURL() + { + return properties.get( "userInfoURL" ); + } + + public String getCertificate() + { + return properties.get( "certificate" ); + } + + public String getAuthenticationCertificate() + { + return properties.get( "authenticationCertificate" ); + } + + //-- Credentials node --// + // In XML this is known as "user" + public String getUUID() + { + return properties.get( "user" ); + } + + public String getUsername() + { + String username = properties.get( "username" ); + if (null == username || username.isBlank()) + { + return properties.get( "method" ); + } + return username; + } + + public String getPKCS12() + { + return properties.get( "pkcs12" ); + } + + public String getLicenseCertificate() + { + return properties.get( "licenseCertificate" ); + } + + public String getPrivateLicenseKey() + { + return properties.get( "privateLicenseKey" ); + } + + public String getLoginMethod() + { + return properties.getOrDefault( "method", "anonymous" ); + } + + //-- Activation Token node --// + public String getDeviceFingerprint() + { + return properties.get( "fingerprint" ); + } + + public String getDeviceUUID() + { + return properties.get( "device" ); + } + + public String getDeviceType() + { + return properties.get( "deviceType" ); + } + + public Map getLicenseServices() + { + return licenseServiceCertificates; + } + + private String getSignature() + { + return properties.get( "signature" ); + } + + /** + * Gets a license certificate associated with a specific URL + * + * @param url see description + * @return The associated license certificate + */ + public String getLicenseServiceCertificate( String url ) + { + return licenseServiceCertificates.getOrDefault( url.trim(), "" ); + } + + public Set getOperatorList() + { + return operatorURLList; + } + + + /** + * Writes the activation.xml file to the appropriate folder based on the properties + * stored in memory + * + * @throws ParserConfigurationException if the document cannot be created + * @throws IOException if the activation file cannot be created or written. + */ + public void updateActivationFile() + throws ParserConfigurationException, IOException + { + DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance(); + docFactory.setNamespaceAware( true ); + DocumentBuilder docBuilder = docFactory.newDocumentBuilder(); + + LOGGER.log( Level.FINEST, "DEBUG: Creating new activation" ); + + Document activationDoc = docBuilder.newDocument(); + // Create XML declaration - handled by Transformer by default + Element activationInfo = activationDoc.createElementNS( XmlUtils.ADOBE_ADEPT_NS, "activationInfo" ); + activationDoc.appendChild( activationInfo ); + + // Create and populate activationServiceInfo Element + // When writing this node to XML, be sure and do it in this order + Element activationServiceInfo = activationDoc + .createElementNS( XmlUtils.ADOBE_ADEPT_NS, "adept:activationServiceInfo" ); + activationInfo.appendChild( activationServiceInfo ); + XmlUtils.appendTextElem( activationServiceInfo, "adept:authURL", getAuthURL() ); + XmlUtils.appendTextElem( activationServiceInfo, "adept:userInfoURL", getUserInfoURL() ); + XmlUtils.appendTextElem( activationServiceInfo, "adept:activationURL", getActivationURL() ); + XmlUtils.appendTextElem( activationServiceInfo, "adept:certificate", getCertificate() ); + XmlUtils.appendTextElem( activationServiceInfo, "adept:authenticationCertificate", + getAuthenticationCertificate() ); + + // Create and populate credentials Element + Element credentialsEl = activationDoc + .createElementNS( XmlUtils.ADOBE_ADEPT_NS, "adept:credentials" ); + activationInfo.appendChild( credentialsEl ); + XmlUtils.appendTextElem( credentialsEl, "adept:user", getUUID() ); + Element nameEl = activationDoc.createElementNS( XmlUtils.ADOBE_ADEPT_NS, "adept:username" ); + credentialsEl.appendChild( nameEl ); + if (!getLoginMethod().equals( "anonymous" )) + { + nameEl.setTextContent( getUsername() ); + } + nameEl.setAttribute( "method", getLoginMethod() ); + XmlUtils.appendTextElem( credentialsEl, "adept:pkcs12", getPKCS12() ); + XmlUtils.appendTextElem( credentialsEl, "adept:licenseCertificate", getLicenseCertificate() ); + XmlUtils.appendTextElem( credentialsEl, "adept:privateLicenseKey", getPrivateLicenseKey() ); + XmlUtils.appendTextElem( credentialsEl, "adept:authenticationCertificate", + getAuthenticationCertificate() ); + + // Create and populate activationToken Element, without namespace + if (hasActivationToken) + { + credentialsEl = activationDoc + .createElement( "activationToken" ); + activationInfo.appendChild( credentialsEl ); + XmlUtils.appendTextElem( credentialsEl, "device", getDeviceUUID() ); + XmlUtils.appendTextElem( credentialsEl, "fingerprint", getDeviceFingerprint() ); + XmlUtils.appendTextElem( credentialsEl, "deviceType", getDeviceType() ); + XmlUtils.appendTextElem( credentialsEl, "activationURL", getActivationURL() ); + XmlUtils.appendTextElem( credentialsEl, "user", getUUID() ); + XmlUtils.appendTextElem( credentialsEl, "signature", getSignature() ); + } + + // Create and populate licenseServices Element + if (!licenseServiceCertificates.isEmpty()) + { + Element licenseServicesNode = activationDoc + .createElementNS( XmlUtils.ADOBE_ADEPT_NS, + "adept:licenseServices" ); + activationInfo.appendChild( licenseServicesNode ); + licenseServiceCertificates.forEach( ( licenseURL, certificate ) -> { + Element licenseServiceInfo = activationDoc. + createElementNS( XmlUtils.ADOBE_ADEPT_NS, "adept:licenseServiceInfo" ); + XmlUtils.appendTextElem( licenseServiceInfo, "adept:licenseURL", licenseURL ); + XmlUtils.appendTextElem( licenseServiceInfo, "adept:certificate", certificate ); + licenseServicesNode.appendChild( licenseServiceInfo ); + } ); + } + + // Create and populate operatorURL list + if (!operatorURLList.isEmpty()) + { + Element operatorURLListNode = activationDoc + .createElementNS( XmlUtils.ADOBE_ADEPT_NS, "adept:operatorURLList" ); + activationInfo.appendChild( operatorURLListNode ); + // add the user id, just to match the linux code. + XmlUtils.appendTextElem( operatorURLListNode, "adept:user", getUUID() ); + operatorURLList.forEach( operatorURL -> XmlUtils + .appendTextElem( operatorURLListNode, "adept:operatorURL", operatorURL ) ); + } + updateActivationFile( activationDoc, activationFile ); + } + + /** + * Updates the activation file on disk with the content of the provided document. + * + * @param doc The XML document to write to the activation file. + */ + @SuppressWarnings("UnusedReturnValue") + private String updateActivationFile( Document doc, Path activationFile ) + throws IOException, GourouException + { + String xmlStr = XmlUtils.docToString( doc, false ); + LOGGER.log( Level.FINE, "DEBUG: Update Activation file:\n" + xmlStr ); + Files.writeString( activationFile, xmlStr ); + return xmlStr; + } + + /** + * checks the stored activation info to see if it's already stored. If not, request + * the properties from Adobe via HTTP + * + * @param ACSServer The URL of the Adobe Content Service + * @return true if the info was updated from the internet, false if it's already in memory. + * If false, no need to update the activation file in the file system. + * + * @throws ParserConfigurationException, IOException, SAXException if an + * error occurs while parsing a new activation info {@link Document} + */ + boolean checkActivationInfo( String ACSServer ) + throws ParserConfigurationException, IOException, SAXException + { + if (properties.size() < 3) + { + // fetch the activationInfo from the Adobe Content Service server + LOGGER.log( Level.FINEST, "DEBUG: Creating new activation" ); + + // Get activationServiceInfo from content server + if (ACSServer == null || ACSServer.isEmpty()) + { // use default value + ACSServer = "https://adeactivate.adobe.com/adept"; // ACS_SERVER + } + String activationURL = ACSServer + "/ActivationServiceInfo"; + Document docActivationServiceInfo = httpUtils + .sendHTTPRequestForXML( activationURL, null, null, null, false ); + + Element docRoot = docActivationServiceInfo.getDocumentElement(); + parseActivationInfo( docRoot ); + return true; + } + else + { + LOGGER.log( Level.FINE, "DEBUG: Read previous activation configuration" ); + } + return false; + } + + /** + * checks the stored authentication info to see if it's already stored. If not, requests + * the properties from Adobe via HTTP + * + * @return true if the info was updated from the internet, false if it's already in memory. + * If false, no need to update the activation file in the file system. + */ + public boolean checkAuthCert() + throws GourouException, IOException, ParserConfigurationException, SAXException + { + // Check for the existence of an authentication certificate + String authenticationCertificate = getAuthenticationCertificate(); + if (null == authenticationCertificate + || authenticationCertificate.isEmpty()) + { + // No authentication certificate stored, get it from Adobe + LOGGER.log( Level.FINEST, "DEBUG: Creating new activation, authentication part" ); + + String authenticationURL = getAuthURL(); + // There will be no authentication URL if checkActivationInfo was not called first. + if (null == authenticationURL) + { + return false; + } + authenticationURL = authenticationURL.trim() + "/AuthenticationServiceInfo"; + + Document docAuthenticationServiceInfo = httpUtils + .sendHTTPRequestForXML( authenticationURL, null, null, + null, false ); + authenticationCertificate = XmlUtils + .extractTextElem( docAuthenticationServiceInfo, "//adept:certificate", true ); + + properties.put( "authenticationCertificate", authenticationCertificate ); + String activationURL = XmlUtils + .extractTextElem( docAuthenticationServiceInfo, "//adept:authURL", true ); + properties.put( "activationURL", activationURL ); + return true; + } + return false; + } + + // unnecessary method, used for testing. + public int getPropertiesSize() + { + return properties.size(); + } +}