001: /*
002: * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
003: *
004: * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
005: *
006: * The contents of this file are subject to the terms of either the GNU
007: * General Public License Version 2 only ("GPL") or the Common
008: * Development and Distribution License("CDDL") (collectively, the
009: * "License"). You may not use this file except in compliance with the
010: * License. You can obtain a copy of the License at
011: * http://www.netbeans.org/cddl-gplv2.html
012: * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
013: * specific language governing permissions and limitations under the
014: * License. When distributing the software, include this License Header
015: * Notice in each file and include the License file at
016: * nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this
017: * particular file as subject to the "Classpath" exception as provided
018: * by Sun in the GPL Version 2 section of the License file that
019: * accompanied this code. If applicable, add the following below the
020: * License Header, with the fields enclosed by brackets [] replaced by
021: * your own identifying information:
022: * "Portions Copyrighted [year] [name of copyright owner]"
023: *
024: * Contributor(s):
025: *
026: * The Original Software is NetBeans. The Initial Developer of the Original
027: * Software is Sun Microsystems, Inc. Portions Copyright 1997-2007 Sun
028: * Microsystems, Inc. All Rights Reserved.
029: *
030: * If you wish your version of this file to be governed by only the CDDL
031: * or only the GPL Version 2, indicate your decision by adding
032: * "[Contributor] elects to include this software in this distribution
033: * under the [CDDL or GPL Version 2] license." If you do not indicate a
034: * single choice of license, a recipient has the option to distribute
035: * your version of this file under either the CDDL, the GPL Version 2 or
036: * to extend the choice of license to its licensees as provided above.
037: * However, if you add GPL Version 2 code and therefore, elected the GPL
038: * Version 2 license, then the option applies only if the new code is
039: * made subject to such option by the copyright holder.
040: */
041:
042: package org.netbeans.nbbuild;
043:
044: import java.io.File;
045: import java.io.IOException;
046: import java.io.InputStream;
047: import java.net.URI;
048: import java.net.URISyntaxException;
049: import java.security.cert.Certificate;
050: import java.util.ArrayList;
051: import java.util.Arrays;
052: import java.util.Enumeration;
053: import java.util.List;
054: import java.util.jar.JarEntry;
055: import java.util.jar.JarFile;
056: import org.apache.tools.ant.BuildException;
057: import org.apache.tools.ant.DirectoryScanner;
058: import org.apache.tools.ant.Project;
059: import org.apache.tools.ant.Task;
060: import org.apache.tools.ant.types.FileSet;
061: import org.w3c.dom.Document;
062: import org.w3c.dom.Element;
063: import org.w3c.dom.NodeList;
064: import org.xml.sax.EntityResolver;
065: import org.xml.sax.ErrorHandler;
066: import org.xml.sax.InputSource;
067: import org.xml.sax.SAXException;
068: import org.xml.sax.SAXParseException;
069:
070: /**
071: * Validates the syntax and semantics of one or more JNLP files.
072: * Any other JNLP fragments referred to recursively from these files will be validated as well.
073: * JNLP files must specify a document type, normally:
074: * <!DOCTYPE jnlp PUBLIC "-//Sun Microsystems, Inc//DTD JNLP Discriptor 1.5//EN" "http://java.sun.com/dtd/JNLP-1.5.dtd">
075: * (including the misspelling: see bug #6613630).
076: * The codebase specified in the file is used as is if a file: URL;
077: * if $$codebase, it is taken to be the immediately containing directory, to match the behavior of JnlpDownloadServlet;
078: * if a remote URL, it is also taken to be the immediately containing directory,
079: * since otherwise it would be impossible to validate files which were generated with intent to upload to a server.
080: * See issue #96630.
081: */
082: public class VerifyJNLP extends Task {
083:
084: private List<FileSet> filesets = new ArrayList<FileSet>();
085:
086: /**
087: * Add one or more JNLP files to validate.
088: * Use <fileset file="..."/> if you have just one.
089: * Fragments referred to from higher JNLP files are checked automatically and do not need to be specified.
090: */
091: public void addConfiguredFileset(FileSet fs) {
092: filesets.add(fs);
093: }
094:
095: public @Override
096: void execute() throws BuildException {
097: for (FileSet fs : filesets) {
098: DirectoryScanner s = fs.getDirectoryScanner(getProject());
099: File basedir = s.getBasedir();
100: for (String incl : s.getIncludedFiles()) {
101: validate(new File(basedir, incl));
102: }
103: }
104: }
105:
106: private void validate(File jnlp) throws BuildException {
107: log("Validating: " + jnlp);
108: Document doc;
109: try {
110: doc = XMLUtil.parse(
111: new InputSource(jnlp.toURI().toString()), true,
112: false, new ErrorHandler() {
113: public void warning(SAXParseException exception)
114: throws SAXException {
115: fatalError(exception);
116: }
117:
118: public void error(SAXParseException exception)
119: throws SAXException {
120: fatalError(exception);
121: }
122:
123: public void fatalError(
124: SAXParseException exception)
125: throws SAXException {
126: throw new SAXException(
127: "parse or validation error:\n"
128: + exception.getSystemId()
129: + ":"
130: + exception.getLineNumber()
131: + ": "
132: + exception.getMessage());
133: }
134: }, new EntityResolver() {
135: public InputSource resolveEntity(
136: String publicId, String systemId)
137: throws SAXException, IOException {
138: if ("-//Sun Microsystems, Inc//DTD JNLP Discriptor 1.5//EN"
139: .equals(publicId)) {
140: return new InputSource(VerifyJNLP.class
141: .getResource("JNLP-1.5.dtd")
142: .toString());
143: } else {
144: return null;
145: }
146: }
147: });
148: } catch (Exception x) {
149: throw new BuildException(x);
150: }
151: String codebase = doc.getDocumentElement().getAttribute(
152: "codebase");
153: URI base;
154: if (codebase.equals("$$codebase")) {
155: base = jnlp.getParentFile().toURI();
156: } else {
157: try {
158: base = new URI(codebase);
159: if (!base.isAbsolute()) {
160: throw new BuildException("JNLP validation error\n"
161: + jnlp + ": non-absolute codebase " + base);
162: }
163: if (!"file".equals(base.getScheme())) {
164: // Needed for local validation of a tree intended for eventual upload to a server.
165: base = jnlp.getParentFile().toURI();
166: }
167: } catch (URISyntaxException x) {
168: throw new BuildException("JNLP validation error\n"
169: + jnlp + ": invalid codebase '" + codebase
170: + "': " + x, x);
171: }
172: }
173: Certificate[] existingCertificates = null;
174: File existingSignedJar = null;
175: NodeList nl = doc.getElementsByTagName("*");
176: for (int i = 0; i < nl.getLength(); i++) {
177: Element el = (Element) nl.item(i);
178: String href = el.getAttribute("href");
179: if (href.length() > 0) {
180: URI u;
181: try {
182: u = base.resolve(new URI(href));
183: } catch (URISyntaxException x) {
184: throw new BuildException("JNLP validation error\n"
185: + jnlp + ": invalid href '" + href + "': "
186: + x, x);
187: }
188: assert u.isAbsolute() : u + " not absolute as " + href
189: + " resolved against " + base;
190: if ("file".equals(u.getScheme())) {
191: File f = new File(u);
192: if (!f.isFile()) {
193: if (el.getTagName().equals("icon")) {
194: // jnlp.xml in harness generates <icon href="${app.icon}"/> optimistically.
195: // Does not seem to be a problem if it is missing.
196: log(jnlp + ": warning: no such file " + f,
197: Project.MSG_WARN);
198: } else {
199: throw new BuildException(
200: "JNLP validation error\n" + jnlp
201: + ": no such file " + f);
202: }
203: }
204: if (el.getTagName().equals("extension")) {
205: validate(f);
206: } else if (el.getTagName().equals("jar")) {
207: try {
208: JarFile jf = new JarFile(f, true);
209: // Try to find signers.
210: try {
211: Enumeration<JarEntry> entries = jf
212: .entries();
213: while (entries.hasMoreElements()) {
214: JarEntry entry = entries
215: .nextElement();
216: if (entry.getName().startsWith(
217: "META-INF/")) {
218: // At least MANIFEST.MF is not signed in the normal way, it seems.
219: continue;
220: }
221: if (entry.getSize() < 1) {
222: // Dirs are not signed.
223: continue;
224: }
225: InputStream is = jf
226: .getInputStream(entry);
227: int read = 0;
228: while (read != -1) {
229: read = is.read();
230: }
231: Certificate[] certs = entry
232: .getCertificates();
233: /*
234: System.err.println("existingSignedJar=" + existingSignedJar + " existingCertificates=" + Arrays.toString(existingCertificates) +
235: " f=" + f + " certs=" + Arrays.toString(certs) + " entry.name=" + entry.getName());
236: */
237: if (existingSignedJar != null
238: && !Arrays
239: .equals(certs,
240: existingCertificates)) {
241: throw new BuildException(
242: "JNLP validation error\n"
243: + jnlp
244: + ": different signatures (or signing status) between "
245: + existingSignedJar
246: + " and " + f);
247: }
248: existingCertificates = certs;
249: existingSignedJar = f;
250: break; // just check one representative file
251: }
252: } finally {
253: jf.close();
254: }
255: } catch (IOException x) {
256: throw new BuildException(
257: "JNLP validation error\n"
258: + jnlp
259: + ": error examining signatures in "
260: + f + ": " + x, x);
261: }
262: }
263: } else {
264: try {
265: u.toURL().openStream().close();
266: } catch (IOException x) {
267: log(jnlp + ": could not open network URL " + u,
268: Project.MSG_WARN);
269: // Do not halt build; might just be a network connectivity issue.
270: }
271: }
272: }
273: }
274: }
275:
276: }
|