/*
* The MIT License
*
* Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package hudson.model;
import com.google.common.io.Resources;
import com.trilead.ssh2.crypto.Base64;
import hudson.ClassicPluginStrategy;
import hudson.Util;
import hudson.model.UsageStatistics.CombinedCipherInputStream;
import hudson.node_monitors.ArchitectureMonitor;
import hudson.util.VersionNumber;
import java.util.Set;
import jenkins.model.Jenkins;
import net.sf.json.JSONObject;
import org.apache.commons.io.IOUtils;
import org.junit.Rule;
import org.junit.Test;
import org.jvnet.hudson.test.JenkinsRule;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.Charset;
import java.security.KeyFactory;
import java.security.interfaces.RSAPrivateKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.zip.GZIPInputStream;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
/**
* @author Kohsuke Kawaguchi
*/
public class UsageStatisticsTest {
@Rule
public JenkinsRule j = new JenkinsRule();
/**
* Makes sure that the stat data can be decrypted safely.
*/
@Test
public void roundtrip() throws Exception {
j.createOnlineSlave();
warmUpNodeMonitorCache();
// key pair for testing
String privateKey = "30820276020100300d06092a864886f70d0101010500048202603082025c0201000281810084cababdb38040f659c2cb07a36d758f46e84ebc3d6ba39d967aedf1d396b0788ed3ab868d45ce280b1102b434c2a250ddc3254defe1785ab4f94d7038cf69ecca16753d2de3f6ad8976b3f74902d8634111d730982da74e1a6e3fc0bc3523bba53e45b8a8cbfd0321b94efc9f7fefbe66ad85281e3d0323d87f4426ec51204f0203010001028180784deaacdea8bd31f2d44578601954be3f714b93c2d977dbd76efb8f71303e249ad12dbeb2d2a1192a1d7923a6010768d7e06a3597b3df83de1d5688eb0f0e58c76070eddd696682730c93890dc727564c65dc8416bfbde5aad4eb7a97ed923efb55a291daf3c00810c0e43851298472fd539aab355af8cedcf1e9a0cbead661024100c498375102b068806c71dec838dc8dfa5624fb8a524a49cffadc19d10689a8c9c26db514faba6f96e50a605122abd3c9af16e82f2b7565f384528c9f31ea5947024100aceafd31d7f4872a873c7e5fe88f20c2fb086a053c6970026b3ce364768e2033100efb1ad8f2010fe53454a29decedc23a8a0c8df347742b1f13e11bd3a284b9024100931321470cd0f6cd24d4278bf8e61f9d69b6ef2bf3163a944aa340f91c7ffdf33aeea22b18cc43514af6714a21bb148d6cdca14530a8fa65acd7a8f62bfc9b5f024067452059f8438dc61466488336fce3f00ec483ad04db638dce45daf850e5a8cd5635dc39b87f2fab32940247ec5167ddabe06e870858104500967ac687aa73e102407e3b7997503e18d8d0f094d5e0bd5d57cb93cb39a2fc42cec1ea9a1562786438b61139e45813204d72c919f5397e139ad051d98e4d0f8a06d237f42c0d8440fb";
String publicKey = "30819f300d06092a864886f70d010101050003818d003081890281810084cababdb38040f659c2cb07a36d758f46e84ebc3d6ba39d967aedf1d396b0788ed3ab868d45ce280b1102b434c2a250ddc3254defe1785ab4f94d7038cf69ecca16753d2de3f6ad8976b3f74902d8634111d730982da74e1a6e3fc0bc3523bba53e45b8a8cbfd0321b94efc9f7fefbe66ad85281e3d0323d87f4426ec51204f0203010001";
String data = new UsageStatistics(publicKey).getStatData();
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
RSAPrivateKey priv = (RSAPrivateKey)keyFactory.generatePrivate(new PKCS8EncodedKeySpec(Util.fromHexString(privateKey)));
byte[] cipherText = Base64.decode(data.toCharArray());
InputStreamReader r = new InputStreamReader(new GZIPInputStream(
new CombinedCipherInputStream(new ByteArrayInputStream(cipherText),priv,"AES")), "UTF-8");
JSONObject o = JSONObject.fromObject(IOUtils.toString(r));
Jenkins jenkins = Jenkins.getActiveInstance();
// A bit intrusive with UsageStatistics internals, but done to prevent undetected changes
// that would cause issues with parsing/analyzing uploaded usage statistics
assertEquals(1, o.getInt("stat"));
assertEquals(jenkins.getLegacyInstanceId(), o.getString("install"));
assertEquals(jenkins.servletContext.getServerInfo(), o.getString("servletContainer"));
assertEquals(Jenkins.VERSION, o.getString("version"));
assertTrue(o.has("plugins"));
assertTrue(o.has("jobs"));
assertTrue(o.has("nodes"));
// Validate the plugins format
List<JSONObject> plugins = sortPlugins((List<JSONObject>) o.get("plugins"));
Set<String> detached = new TreeSet<>();
for (ClassicPluginStrategy.DetachedPlugin p: ClassicPluginStrategy.getDetachedPlugins()) {
if (p.getSplitWhen().isOlderThan(Jenkins.getVersion())) {
detached.add(p.getShortName());
}
}
Set<String> keys = new TreeSet<>();
keys.add("name");
keys.add("version");
Set<String> reported = new TreeSet<>();
for (JSONObject plugin: plugins) {
assertThat(plugin.keySet(), is((Set)keys));
assertThat(plugin.get("name"), instanceOf(String.class));
assertThat(plugin.get("version"), instanceOf(String.class));
String name = plugin.getString("name");
assertThat("No duplicates", reported.contains(name), is(false));
reported.add(name);
}
reported.retainAll(detached); // ignore the dependencies of the detached plugins
assertThat(reported, is(detached));
// Compare content to watch out for backwards compatibility
compareWithFile("jobs.json", sortJobTypes((JSONObject) o.get("jobs")));
compareWithFile("nodes.json", o.get("nodes"));
}
/**
* Node monitoring uses a cache for retrieved values. Querying it the first time will return null, while triggering
* a background update of that cache.
* <p/>
* <p>This method triggers that update and waits until the cache is filled during roughly 1 second max.</p>
*
* @throws InterruptedException
*/
private void warmUpNodeMonitorCache() throws InterruptedException {
Jenkins j = Jenkins.getActiveInstance();
ArchitectureMonitor.DescriptorImpl descriptor = j.getDescriptorByType(ArchitectureMonitor.DescriptorImpl.class);
String value = null;
int count = 1;
while (value == null && count++ <= 5) // If for some reason the cache doesn't get populated, don't loop forever
{
final Computer master = j.getComputers()[0];
value = descriptor.get(master);
Thread.sleep(200);
}
}
// Plugins can be retrieved in any order, so sorting them so that the test is stable
private List<JSONObject> sortPlugins(List<JSONObject> list) {
List<JSONObject> sorted = new ArrayList<>(list);
Collections.sort(sorted, new Comparator<JSONObject>() {
public int compare(JSONObject j1, JSONObject j2) {
return j1.getString("name").compareTo(j2.getString("name"));
}
});
return sorted;
}
private JSONObject sortJobTypes(JSONObject object) {
SortedSet<String> keys = new TreeSet(object.keySet());
JSONObject sorted = new JSONObject();
for (String key : keys) {
sorted.put(key, object.get(key));
}
return sorted;
}
private void compareWithFile(String fileName, Object object) throws IOException {
Class clazz = this.getClass();
String fileContent = Resources.toString(clazz.getResource(clazz.getSimpleName() + "/" + fileName), Charset.forName("UTF-8"));
fileContent = fileContent.replace("JVMVENDOR", System.getProperty("java.vendor"));
fileContent = fileContent.replace("JVMNAME", System.getProperty("java.vm.name"));
fileContent = fileContent.replace("JVMVERSION", System.getProperty("java.version"));
String os = System.getProperty("os.name");
String arch = System.getProperty("os.arch");
fileContent = fileContent.replace("OSSPEC", os + " (" + arch + ')');
assertEquals(fileContent, object.toString());
}
}