~ubuntu-branches/ubuntu/trusty/jenkins/trusty

« back to all changes in this revision

Viewing changes to core/src/main/java/hudson/model/UpdateSite.java

  • Committer: Package Import Robot
  • Author(s): James Page
  • Date: 2013-08-13 12:35:19 UTC
  • mfrom: (1.1.13)
  • Revision ID: package-import@ubuntu.com-20130813123519-tizgfxcr70trl7r0
Tags: 1.509.2+dfsg-1
* New upstream release (Closes: #706725):
  - d/control: Update versioned BD's:
    * jenkins-executable-war >= 1.28.
    * jenkins-instance-identity >= 1.3.
    * libjenkins-remoting-java >= 2.23.
    * libjenkins-winstone-java >= 0.9.10-jenkins-44.
    * libstapler-java >= 1.207.
    * libjenkins-json-java >= 2.4-jenkins-1.
    * libstapler-adjunct-timeline-java >= 1.4.
    * libstapler-adjunct-codemirror-java >= 1.2.
    * libmaven-hpi-plugin-java >= 1.93.
    * libjenkins-xstream-java >= 1.4.4-jenkins-3.
  - d/maven.rules: Map to older version of animal-sniffer-maven-plugin.
  - Add patch for compatibility with guava >= 0.14.
  - Add patch to exclude asm4 dependency via jnr-posix.
  - Fixes the following security vulnerabilities:
    CVE-2013-2034, CVE-2013-2033, CVE-2013-2034, CVE-2013-1808
* d/patches/*: Switch to using git patch-queue for managing patches.
* De-duplicate jars between libjenkins-java and jenkins-external-job-monitor
  (Closes: #701163):
  - d/control: Add dependency between jenkins-external-job-monitor ->
    libjenkins-java.
  - d/rules: 
    Drop installation of jenkins-core in jenkins-external-job-monitor.
  - d/jenkins-external-job-monitor.{links,install}: Link to jenkins-core
    in /usr/share/java instead of included version.
* Wait longer for jenkins to stop during restarts (Closes: #704848):
  - d/jenkins.init: Re-sync init script from upstream codebase.

Show diffs side-by-side

added added

removed removed

Lines of Context:
25
25
 
26
26
package hudson.model;
27
27
 
28
 
import com.trilead.ssh2.crypto.Base64;
29
28
import hudson.PluginManager;
30
29
import hudson.PluginWrapper;
 
30
import hudson.ProxyConfiguration;
31
31
import hudson.lifecycle.Lifecycle;
32
32
import hudson.model.UpdateCenter.UpdateCenterJob;
33
33
import hudson.util.FormValidation;
37
37
import hudson.util.TextFile;
38
38
import hudson.util.VersionNumber;
39
39
import jenkins.model.Jenkins;
 
40
import jenkins.util.JSONSignatureValidator;
40
41
import net.sf.json.JSONException;
41
42
import net.sf.json.JSONObject;
42
 
import org.apache.commons.io.output.NullOutputStream;
43
 
import org.apache.commons.io.output.TeeOutputStream;
44
 
import org.jvnet.hudson.crypto.CertificateUtil;
45
 
import org.jvnet.hudson.crypto.SignatureOutputStream;
46
43
import org.kohsuke.stapler.DataBoundConstructor;
47
44
import org.kohsuke.stapler.HttpResponse;
48
45
import org.kohsuke.stapler.StaplerRequest;
50
47
import org.kohsuke.stapler.export.ExportedBean;
51
48
import org.kohsuke.stapler.interceptor.RequirePOST;
52
49
 
53
 
import java.io.ByteArrayInputStream;
54
50
import java.io.File;
55
 
import java.io.FileInputStream;
56
51
import java.io.IOException;
57
 
import java.io.OutputStreamWriter;
58
 
import java.security.DigestOutputStream;
 
52
import java.io.InputStream;
 
53
import java.net.URI;
 
54
import java.net.URL;
 
55
import java.net.URLConnection;
 
56
import java.net.URLEncoder;
59
57
import java.security.GeneralSecurityException;
60
 
import java.security.MessageDigest;
61
 
import java.security.Signature;
62
 
import java.security.cert.CertificateExpiredException;
63
 
import java.security.cert.CertificateFactory;
64
 
import java.security.cert.CertificateNotYetValidException;
65
 
import java.security.cert.TrustAnchor;
66
 
import java.security.cert.X509Certificate;
67
58
import java.util.ArrayList;
68
59
import java.util.Collections;
69
60
import java.util.HashMap;
70
 
import java.util.HashSet;
71
61
import java.util.List;
72
62
import java.util.Map;
73
63
import java.util.Set;
74
64
import java.util.TreeMap;
 
65
import java.util.concurrent.Callable;
75
66
import java.util.concurrent.Future;
76
67
import java.util.logging.Level;
77
68
import java.util.logging.Logger;
94
85
public class UpdateSite {
95
86
    /**
96
87
     * What's the time stamp of data file?
 
88
     * 0 means never.
97
89
     */
98
 
    private transient long dataTimestamp = -1;
 
90
    private transient volatile long dataTimestamp;
99
91
 
100
92
    /**
101
93
     * When was the last time we asked a browser to check the data for us?
 
94
     * 0 means never.
102
95
     *
103
96
     * <p>
104
97
     * There's normally some delay between when we send HTML that includes the check code,
105
98
     * until we get the data back, so this variable is used to avoid asking too many browseres
106
99
     * all at once.
107
100
     */
108
 
    private transient volatile long lastAttempt = -1;
 
101
    private transient volatile long lastAttempt;
109
102
 
110
103
    /**
111
104
     * If the attempt to fetch data fails, we progressively use longer time out before retrying,
121
114
    /**
122
115
     * Latest data as read from the data file.
123
116
     */
124
 
    private Data data;
 
117
    private transient Data data;
125
118
 
126
119
    /**
127
120
     * ID string for this update source.
139
132
    }
140
133
 
141
134
    /**
142
 
     * When read back from XML, initialize them back to -1.
143
 
     */
144
 
    private Object readResolve() {
145
 
        dataTimestamp = lastAttempt = -1;
146
 
        return this;
147
 
    }
148
 
 
149
 
    /**
150
135
     * Get ID string.
151
136
     */
152
137
    @Exported
156
141
 
157
142
    @Exported
158
143
    public long getDataTimestamp() {
 
144
        assert dataTimestamp >= 0;
159
145
        return dataTimestamp;
160
146
    }
161
147
 
162
148
    /**
 
149
     * Update the data file from the given URL if the file
 
150
     * does not exist, or is otherwise due for update.
 
151
     * Accepted formats are JSONP or HTML with {@code postMessage}, not raw JSON.
 
152
     * @param signatureCheck whether to enforce the signature (may be off only for testing!)
 
153
     * @return null if no updates are necessary, or the future result
 
154
     * @since 1.502
 
155
     */
 
156
    public Future<FormValidation> updateDirectly(final boolean signatureCheck) {
 
157
        if (! getDataFile().exists() || isDue()) {
 
158
            return Jenkins.getInstance().getUpdateCenter().updateService.submit(new Callable<FormValidation>() {
 
159
                
 
160
                public FormValidation call() throws Exception {
 
161
                    URL src = new URL(getUrl() + "?id=" + URLEncoder.encode(getId(),"UTF-8") 
 
162
                            + "&version="+URLEncoder.encode(Jenkins.VERSION, "UTF-8"));
 
163
                    URLConnection conn = ProxyConfiguration.open(src);
 
164
                    InputStream is = conn.getInputStream();
 
165
                    try {
 
166
                        String uncleanJson = IOUtils.toString(is,"UTF-8");
 
167
                        int jsonStart = uncleanJson.indexOf("{\"");
 
168
                        if (jsonStart >= 0) {
 
169
                            uncleanJson = uncleanJson.substring(jsonStart);
 
170
                            int end = uncleanJson.lastIndexOf('}');
 
171
                            if (end>0)
 
172
                                uncleanJson = uncleanJson.substring(0,end+1);
 
173
                            return updateData(uncleanJson, signatureCheck);
 
174
                        } else {
 
175
                            throw new IOException("Could not find json in content of " +
 
176
                                        "update center from url: "+src.toExternalForm());
 
177
                        }
 
178
                    } finally {
 
179
                        if (is != null)
 
180
                            is.close();
 
181
                    }
 
182
                }
 
183
            });
 
184
        }
 
185
            return null;
 
186
    }
 
187
    
 
188
    /**
163
189
     * This is the endpoint that receives the update center data file from the browser.
164
190
     */
165
191
    public FormValidation doPostBack(StaplerRequest req) throws IOException, GeneralSecurityException {
 
192
        return updateData(IOUtils.toString(req.getInputStream(),"UTF-8"), true);
 
193
    }
 
194
 
 
195
    private FormValidation updateData(String json, boolean signatureCheck)
 
196
            throws IOException {
 
197
 
166
198
        dataTimestamp = System.currentTimeMillis();
167
 
        String json = IOUtils.toString(req.getInputStream(),"UTF-8");
 
199
 
168
200
        JSONObject o = JSONObject.fromObject(json);
169
201
 
170
 
        int v = o.getInt("updateCenterVersion");
171
 
        if(v !=1)
172
 
            throw new IllegalArgumentException("Unrecognized update center version: "+v);
 
202
        try {
 
203
            int v = o.getInt("updateCenterVersion");
 
204
            if (v != 1) {
 
205
                throw new IllegalArgumentException("Unrecognized update center version: " + v);
 
206
            }
 
207
        } catch (JSONException x) {
 
208
            throw new IllegalArgumentException("Could not find (numeric) updateCenterVersion in " + json, x);
 
209
        }
173
210
 
174
211
        if (signatureCheck) {
175
212
            FormValidation e = verifySignature(o);
176
213
            if (e.kind!=Kind.OK) {
177
 
                LOGGER.severe(e.renderHtml());
 
214
                LOGGER.severe(e.toString());
178
215
                return e;
179
216
            }
180
217
        }
193
230
     * Verifies the signature in the update center data file.
194
231
     */
195
232
    private FormValidation verifySignature(JSONObject o) throws IOException {
196
 
        try {
197
 
            FormValidation warning = null;
198
 
 
199
 
            JSONObject signature = o.getJSONObject("signature");
200
 
            if (signature.isNullObject()) {
201
 
                return FormValidation.error("No signature block found in update center '"+id+"'");
202
 
            }
203
 
            o.remove("signature");
204
 
 
205
 
            List<X509Certificate> certs = new ArrayList<X509Certificate>();
206
 
            {// load and verify certificates
207
 
                CertificateFactory cf = CertificateFactory.getInstance("X509");
208
 
                for (Object cert : signature.getJSONArray("certificates")) {
209
 
                    X509Certificate c = (X509Certificate) cf.generateCertificate(new ByteArrayInputStream(Base64.decode(cert.toString().toCharArray())));
210
 
                    try {
211
 
                        c.checkValidity();
212
 
                    } catch (CertificateExpiredException e) { // even if the certificate isn't valid yet, we'll proceed it anyway
213
 
                        warning = FormValidation.warning(e,String.format("Certificate %s has expired in update center '%s'",cert.toString(),id));
214
 
                    } catch (CertificateNotYetValidException e) {
215
 
                        warning = FormValidation.warning(e,String.format("Certificate %s is not yet valid in update center '%s'",cert.toString(),id));
216
 
                    }
217
 
                    certs.add(c);
218
 
                }
219
 
 
220
 
                // if we trust default root CAs, we end up trusting anyone who has a valid certificate,
221
 
                // which isn't useful at all
222
 
                Set<TrustAnchor> anchors = new HashSet<TrustAnchor>(); // CertificateUtil.getDefaultRootCAs();
223
 
                Jenkins j = Jenkins.getInstance();
224
 
                for (String cert : (Set<String>) j.servletContext.getResourcePaths("/WEB-INF/update-center-rootCAs")) {
225
 
                    if (cert.endsWith(".txt"))  continue;       // skip text files that are meant to be documentation
226
 
                    anchors.add(new TrustAnchor((X509Certificate)cf.generateCertificate(j.servletContext.getResourceAsStream(cert)),null));
227
 
                }
228
 
                File[] cas = new File(j.root, "update-center-rootCAs").listFiles();
229
 
                if (cas!=null) {
230
 
                    for (File cert : cas) {
231
 
                        if (cert.getName().endsWith(".txt"))  continue;       // skip text files that are meant to be documentation
232
 
                        FileInputStream in = new FileInputStream(cert);
233
 
                        try {
234
 
                            anchors.add(new TrustAnchor((X509Certificate)cf.generateCertificate(in),null));
235
 
                        } finally {
236
 
                            in.close();
237
 
                        }
238
 
                    }
239
 
                }
240
 
                CertificateUtil.validatePath(certs,anchors);
241
 
            }
242
 
 
243
 
            // this is for computing a digest to check sanity
244
 
            MessageDigest sha1 = MessageDigest.getInstance("SHA1");
245
 
            DigestOutputStream dos = new DigestOutputStream(new NullOutputStream(),sha1);
246
 
 
247
 
            // this is for computing a signature
248
 
            Signature sig = Signature.getInstance("SHA1withRSA");
249
 
            sig.initVerify(certs.get(0));
250
 
            SignatureOutputStream sos = new SignatureOutputStream(sig);
251
 
 
252
 
            // until JENKINS-11110 fix, UC used to serve invalid digest (and therefore unverifiable signature)
253
 
            // that only covers the earlier portion of the file. This was caused by the lack of close() call
254
 
            // in the canonical writing, which apparently leave some bytes somewhere that's not flushed to
255
 
            // the digest output stream. This affects Jenkins [1.424,1,431].
256
 
            // Jenkins 1.432 shipped with the "fix" (1eb0c64abb3794edce29cbb1de50c93fa03a8229) that made it
257
 
            // compute the correct digest, but it breaks all the existing UC json metadata out there. We then
258
 
            // quickly discovered ourselves in the catch-22 situation. If we generate UC with the correct signature,
259
 
            // it'll cut off [1.424,1.431] from the UC. But if we don't, we'll cut off [1.432,*).
260
 
            //
261
 
            // In 1.433, we revisited 1eb0c64abb3794edce29cbb1de50c93fa03a8229 so that the original "digest"/"signature"
262
 
            // pair continues to be generated in a buggy form, while "correct_digest"/"correct_signature" are generated
263
 
            // correctly.
264
 
            //
265
 
            // Jenkins should ignore "digest"/"signature" pair. Accepting it creates a vulnerability that allows
266
 
            // the attacker to inject a fragment at the end of the json.
267
 
            o.writeCanonical(new OutputStreamWriter(new TeeOutputStream(dos,sos),"UTF-8")).close();
268
 
 
269
 
            // did the digest match? this is not a part of the signature validation, but if we have a bug in the c14n
270
 
            // (which is more likely than someone tampering with update center), we can tell
271
 
            String computedDigest = new String(Base64.encode(sha1.digest()));
272
 
            String providedDigest = signature.optString("correct_digest");
273
 
            if (providedDigest==null) {
274
 
                return FormValidation.error("No correct_digest parameter in update center '"+id+"'. This metadata appears to be old.");
275
 
            }
276
 
            if (!computedDigest.equalsIgnoreCase(providedDigest)) {
277
 
                return FormValidation.error("Digest mismatch: "+computedDigest+" vs "+providedDigest+" in update center '"+id+"'");
278
 
            }
279
 
 
280
 
            String providedSignature = signature.getString("correct_signature");
281
 
            if (!sig.verify(Base64.decode(providedSignature.toCharArray()))) {
282
 
                return FormValidation.error("Signature in the update center doesn't match with the certificate in update center '"+id+"'");
283
 
            }
284
 
 
285
 
            if (warning!=null)  return warning;
286
 
            return FormValidation.ok();
287
 
        } catch (GeneralSecurityException e) {
288
 
            return FormValidation.error(e,"Signature verification failed in the update center '"+id+"'");
289
 
        }
 
233
        return new JSONSignatureValidator("update site '"+id+"'").verifySignature(o);
290
234
    }
291
235
 
292
236
    /**
294
238
     */
295
239
    public boolean isDue() {
296
240
        if(neverUpdate)     return false;
297
 
        if(dataTimestamp==-1)
 
241
        if(dataTimestamp == 0)
298
242
            dataTimestamp = getDataFile().file.lastModified();
299
243
        long now = System.currentTimeMillis();
300
244
        
487
431
     * Is this the legacy default update center site?
488
432
     */
489
433
    public boolean isLegacyDefault() {
490
 
        return id.equals("default") && url.startsWith("http://hudson-ci.org/") || url.startsWith("http://updates.hudson-labs.org/");
 
434
        return id.equals(UpdateCenter.ID_DEFAULT) && url.startsWith("http://hudson-ci.org/") || url.startsWith("http://updates.hudson-labs.org/");
491
435
    }
492
436
 
493
437
    /**
516
460
 
517
461
        Data(JSONObject o) {
518
462
            this.sourceId = (String)o.get("id");
519
 
            if (sourceId.equals("default")) {
520
 
                core = new Entry(sourceId, o.getJSONObject("core"));
521
 
            }
522
 
            else {
 
463
            JSONObject c = o.optJSONObject("core");
 
464
            if (c!=null) {
 
465
                core = new Entry(sourceId, c, url);
 
466
            } else {
523
467
                core = null;
524
468
            }
525
469
            for(Map.Entry<String,JSONObject> e : (Set<Map.Entry<String,JSONObject>>)o.getJSONObject("plugins").entrySet()) {
569
513
        public final String url;
570
514
 
571
515
        public Entry(String sourceId, JSONObject o) {
 
516
            this(sourceId, o, null);
 
517
        }
 
518
 
 
519
        Entry(String sourceId, JSONObject o, String baseURL) {
572
520
            this.sourceId = sourceId;
573
521
            this.name = o.getString("name");
574
522
            this.version = o.getString("version");
575
 
            this.url = o.getString("url");
 
523
            String url = o.getString("url");
 
524
            if (!URI.create(url).isAbsolute()) {
 
525
                if (baseURL == null) {
 
526
                    throw new IllegalArgumentException("Cannot resolve " + url + " without a base URL");
 
527
                }
 
528
                url = URI.create(baseURL).resolve(url).toString();
 
529
            }
 
530
            this.url = url;
576
531
        }
577
532
 
578
533
        /**
644
599
        
645
600
        @DataBoundConstructor
646
601
        public Plugin(String sourceId, JSONObject o) {
647
 
            super(sourceId, o);
 
602
            super(sourceId, o, UpdateSite.this.url);
648
603
            this.wiki = get(o,"wiki");
649
604
            this.title = get(o,"title");
650
605
            this.excerpt = get(o,"excerpt");
770
725
         * @param dynamicLoad
771
726
         *      If true, the plugin will be dynamically loaded into this Jenkins. If false,
772
727
         *      the plugin will only take effect after the reboot.
 
728
         *      See {@link UpdateCenter#isRestartRequiredForCompletion()}
773
729
         */
774
730
        public Future<UpdateCenterJob> deploy(boolean dynamicLoad) {
775
731
            Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER);
776
732
            UpdateCenter uc = Jenkins.getInstance().getUpdateCenter();
777
733
            for (Plugin dep : getNeededDependencies()) {
778
 
                LOGGER.log(Level.WARNING, "Adding dependent install of " + dep.name + " for plugin " + name);
779
 
                dep.deploy(dynamicLoad);
 
734
                UpdateCenter.InstallationJob job = uc.getJob(dep);
 
735
                if (job == null || job.status instanceof UpdateCenter.DownloadJob.Failure) {
 
736
                    LOGGER.log(Level.WARNING, "Adding dependent install of " + dep.name + " for plugin " + name);
 
737
                    dep.deploy(dynamicLoad);
 
738
                } else {
 
739
                    LOGGER.log(Level.WARNING, "Dependent install of " + dep.name + " for plugin " + name + " already added, skipping");
 
740
                }
780
741
            }
781
742
            return uc.addJob(uc.new InstallationJob(this, UpdateSite.this, Jenkins.getAuthentication(), dynamicLoad));
782
743
        }
821
782
    // The name uses UpdateCenter for compatibility reason.
822
783
    public static boolean neverUpdate = Boolean.getBoolean(UpdateCenter.class.getName()+".never");
823
784
 
824
 
    /**
825
 
     * Off by default until we know this is reasonably working.
826
 
     */
827
 
    public static boolean signatureCheck = true; // Boolean.getBoolean(UpdateCenter.class.getName()+".signatureCheck");
828
785
}