提交 a65bc64d 编写于 作者: K Kohsuke Kawaguchi

Merge branch 'master' of github.com:jenkinsci/jenkins

......@@ -55,7 +55,13 @@ Upcoming changes</a>
<!-- Record your changes in the trunk here. -->
<div id="trunk" style="display:none"><!--=TRUNK-BEGIN=-->
<ul class=image>
<li class=>
<li class=rfe>
Ability for custom view types to disable automatic refresh.
(<a href="https://issues.jenkins-ci.org/browse/JENKINS-21190">issue 21190</a>)
(<a href="https://issues.jenkins-ci.org/browse/JENKINS-21191">issue 21191</a>)
<li class=rfe>
Option to download metadata directly from Jenkins rather than going through the browser.
(<a href="https://issues.jenkins-ci.org/browse/JENKINS-19081">issue 19081</a>)
</ul>
</div><!--=TRUNK-END=-->
......
......@@ -108,7 +108,11 @@ import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
import static hudson.init.InitMilestone.*;
import hudson.model.DownloadService;
import hudson.util.FormValidation;
import static java.util.logging.Level.WARNING;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
/**
* Manages {@link PluginWrapper}s.
......@@ -793,6 +797,24 @@ public abstract class PluginManager extends AbstractModelObject implements OnMas
}
}
@Restricted(NoExternalUse.class)
@RequirePOST public HttpResponse doCheckUpdatesServer() throws IOException {
for (UpdateSite site : Jenkins.getInstance().getUpdateCenter().getSites()) {
FormValidation v = site.updateDirectlyNow(DownloadService.signatureCheck);
if (v.kind != FormValidation.Kind.OK) {
// TODO crude but enough for now
return v;
}
}
for (DownloadService.Downloadable d : DownloadService.Downloadable.all()) {
FormValidation v = d.updateNow();
if (v.kind != FormValidation.Kind.OK) {
return v;
}
}
return HttpResponses.forwardToPreviousPage();
}
protected String identifyPluginShortName(File t) {
try {
JarFile j = new JarFile(t);
......
......@@ -26,26 +26,30 @@ package hudson.model;
import hudson.Extension;
import hudson.ExtensionList;
import hudson.ExtensionPoint;
import hudson.ProxyConfiguration;
import hudson.util.FormValidation;
import hudson.util.FormValidation.Kind;
import hudson.util.QuotedStringTokenizer;
import hudson.util.TextFile;
import jenkins.model.Jenkins;
import jenkins.util.JSONSignatureValidator;
import net.sf.json.JSONException;
import org.kohsuke.stapler.Stapler;
import static hudson.util.TimeUnit2.DAYS;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLEncoder;
import java.util.logging.Logger;
import jenkins.model.DownloadSettings;
import jenkins.model.Jenkins;
import jenkins.util.JSONSignatureValidator;
import net.sf.json.JSONException;
import net.sf.json.JSONObject;
import org.apache.commons.io.IOUtils;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
import org.kohsuke.stapler.Stapler;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import static hudson.util.TimeUnit2.DAYS;
import org.apache.commons.io.IOUtils;
/**
* Service for plugins to periodically retrieve update data files
* (like the one in the update center) through browsers.
......@@ -62,6 +66,9 @@ public class DownloadService extends PageDecorator {
* Builds up an HTML fragment that starts all the download jobs.
*/
public String generateFragment() {
if (!DownloadSettings.get().isUseBrowser()) {
return "";
}
if (neverUpdate) return "";
if (doesNotSupportPostMessage()) return "";
......@@ -135,6 +142,34 @@ public class DownloadService extends PageDecorator {
return null;
}
/**
* Loads JSON from a JSONP URL.
* Metadata for downloadables and update centers is offered in two formats, both designed for download from the browser (predating {@link DownloadSettings}):
* HTML using {@code postMessage} for newer browsers, and JSONP as a fallback.
* Confusingly, the JSONP files are given the {@code *.json} file extension, when they are really JavaScript and should be {@code *.js}.
* This method extracts the JSON from a JSONP URL, since that is what we actually want when we download from the server.
* (Currently the true JSON is not published separately, and extracting from the {@code *.json.html} is more work.)
* @param src a URL to a JSONP file (typically including {@code id} and {@code version} query parameters)
* @return the embedded JSON text
* @throws IOException if either downloading or processing failed
*/
@Restricted(NoExternalUse.class)
public static String loadJSON(URL src) throws IOException {
InputStream is = ProxyConfiguration.open(src).getInputStream();
try {
String jsonp = IOUtils.toString(is, "UTF-8");
int start = jsonp.indexOf('{');
int end = jsonp.lastIndexOf('}');
if (start >= 0 && end > start) {
return jsonp.substring(start, end + 1);
} else {
throw new IOException("Could not find JSON in " + src);
}
} finally {
is.close();
}
}
/**
* Represents a periodically updated JSON data file obtained from a remote URL.
*
......@@ -248,17 +283,28 @@ public class DownloadService extends PageDecorator {
* This is where the browser sends us the data.
*/
public void doPostBack(StaplerRequest req, StaplerResponse rsp) throws IOException {
if (!DownloadSettings.get().isUseBrowser()) {
throw new IOException("not allowed");
}
long dataTimestamp = System.currentTimeMillis();
due = dataTimestamp+getInterval(); // success or fail, don't try too often
String json = IOUtils.toString(req.getInputStream(),"UTF-8");
FormValidation e = load(json, dataTimestamp);
if (e.kind != Kind.OK) {
LOGGER.severe(e.renderHtml());
throw e;
}
rsp.setContentType("text/plain"); // So browser won't try to parse response
}
private FormValidation load(String json, long dataTimestamp) throws IOException {
JSONObject o = JSONObject.fromObject(json);
if (signatureCheck) {
FormValidation e = new JSONSignatureValidator("downloadable '"+id+"'").verifySignature(o);
if (e.kind!= Kind.OK) {
LOGGER.severe(e.renderHtml());
throw e;
return e;
}
}
......@@ -266,8 +312,12 @@ public class DownloadService extends PageDecorator {
df.write(json);
df.file.setLastModified(dataTimestamp);
LOGGER.info("Obtained the updated data file for "+id);
return FormValidation.ok();
}
rsp.setContentType("text/plain"); // So browser won't try to parse response
@Restricted(NoExternalUse.class)
public FormValidation updateNow() throws IOException {
return load(loadJSON(new URL(getUrl() + "?id=" + URLEncoder.encode(getId(), "UTF-8") + "&version=" + URLEncoder.encode(Jenkins.VERSION, "UTF-8"))), System.currentTimeMillis());
}
/**
......@@ -296,7 +346,10 @@ public class DownloadService extends PageDecorator {
public static boolean neverUpdate = Boolean.getBoolean(DownloadService.class.getName()+".never");
/**
* Off by default until we know this is reasonably working.
* May be used to temporarily disable signature checking on {@link DownloadService} and {@link UpdateCenter}.
* Useful when upstream signatures are broken, such as due to expired certificates.
* Should only be used when {@link DownloadSettings#isUseBrowser};
* disabling signature checks for in-browser downloads is <em>very dangerous</em> as unprivileged users could submit spoofed metadata!
*/
public static boolean signatureCheck = !Boolean.getBoolean(DownloadService.class.getName()+".noSignatureCheck");
}
......
......@@ -630,7 +630,7 @@ public class UpdateCenter extends AbstractModelObject implements Saveable, OnMas
public List<FormValidation> updateAllSites() throws InterruptedException, ExecutionException {
List <Future<FormValidation>> futures = new ArrayList<Future<FormValidation>>();
for (UpdateSite site : getSites()) {
Future<FormValidation> future = site.updateDirectly(true);
Future<FormValidation> future = site.updateDirectly(DownloadService.signatureCheck);
if (future != null) {
futures.add(future);
}
......
......@@ -27,32 +27,18 @@ package hudson.model;
import hudson.PluginManager;
import hudson.PluginWrapper;
import hudson.ProxyConfiguration;
import hudson.lifecycle.Lifecycle;
import hudson.model.UpdateCenter.UpdateCenterJob;
import hudson.util.FormValidation;
import hudson.util.FormValidation.Kind;
import hudson.util.HttpResponses;
import hudson.util.TextFile;
import static hudson.util.TimeUnit2.*;
import hudson.util.VersionNumber;
import jenkins.model.Jenkins;
import jenkins.util.JSONSignatureValidator;
import net.sf.json.JSONException;
import net.sf.json.JSONObject;
import org.apache.commons.lang.StringUtils;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.HttpResponse;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.export.Exported;
import org.kohsuke.stapler.export.ExportedBean;
import org.kohsuke.stapler.interceptor.RequirePOST;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLEncoder;
import java.security.GeneralSecurityException;
import java.util.ArrayList;
......@@ -66,10 +52,23 @@ import java.util.concurrent.Callable;
import java.util.concurrent.Future;
import java.util.logging.Level;
import java.util.logging.Logger;
import static hudson.util.TimeUnit2.*;
import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
import jenkins.model.Jenkins;
import jenkins.model.DownloadSettings;
import jenkins.util.JSONSignatureValidator;
import net.sf.json.JSONException;
import net.sf.json.JSONObject;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.HttpResponse;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.export.Exported;
import org.kohsuke.stapler.export.ExportedBean;
import org.kohsuke.stapler.interceptor.RequirePOST;
/**
* Source of the update center information, like "http://jenkins-ci.org/update-center.json"
......@@ -154,42 +153,30 @@ public class UpdateSite {
* @return null if no updates are necessary, or the future result
* @since 1.502
*/
public Future<FormValidation> updateDirectly(final boolean signatureCheck) {
public @CheckForNull Future<FormValidation> updateDirectly(final boolean signatureCheck) {
if (! getDataFile().exists() || isDue()) {
return Jenkins.getInstance().getUpdateCenter().updateService.submit(new Callable<FormValidation>() {
public FormValidation call() throws Exception {
URL src = new URL(getUrl() + "?id=" + URLEncoder.encode(getId(),"UTF-8")
+ "&version="+URLEncoder.encode(Jenkins.VERSION, "UTF-8"));
URLConnection conn = ProxyConfiguration.open(src);
InputStream is = conn.getInputStream();
try {
String uncleanJson = IOUtils.toString(is,"UTF-8");
int jsonStart = uncleanJson.indexOf("{\"");
if (jsonStart >= 0) {
uncleanJson = uncleanJson.substring(jsonStart);
int end = uncleanJson.lastIndexOf('}');
if (end>0)
uncleanJson = uncleanJson.substring(0,end+1);
return updateData(uncleanJson, signatureCheck);
} else {
throw new IOException("Could not find json in content of " +
"update center from url: "+src.toExternalForm());
}
} finally {
if (is != null)
is.close();
}
@Override public FormValidation call() throws Exception {
return updateDirectlyNow(signatureCheck);
}
});
}
} else {
return null;
}
}
@Restricted(NoExternalUse.class)
public @Nonnull FormValidation updateDirectlyNow(boolean signatureCheck) throws IOException {
return updateData(DownloadService.loadJSON(new URL(getUrl() + "?id=" + URLEncoder.encode(getId(), "UTF-8") + "&version=" + URLEncoder.encode(Jenkins.VERSION, "UTF-8"))), signatureCheck);
}
/**
* This is the endpoint that receives the update center data file from the browser.
*/
public FormValidation doPostBack(StaplerRequest req) throws IOException, GeneralSecurityException {
if (!DownloadSettings.get().isUseBrowser()) {
throw new IOException("not allowed");
}
return updateData(IOUtils.toString(req.getInputStream(),"UTF-8"), true);
}
......
......@@ -374,6 +374,15 @@ public abstract class View extends AbstractModelObject implements AccessControll
return true;
}
/**
* Enables or disables automatic refreshes of the view.
* By default, automatic refreshes are enabled.
* @since 1.557
*/
public boolean isAutomaticRefreshEnabled() {
return true;
}
/**
* If true, only show relevant executors
*/
......
......@@ -239,9 +239,13 @@ public abstract class FormValidation extends IOException implements HttpResponse
return ok();
return new FormValidation(kind, message) {
public String renderHtml() {
StaplerRequest req = Stapler.getCurrentRequest();
if (req == null) { // being called from some other context
return message;
}
// 1x16 spacer needed for IE since it doesn't support min-height
return "<div class="+ kind.name().toLowerCase(Locale.ENGLISH) +"><img src='"+
Stapler.getCurrentRequest().getContextPath()+ Jenkins.RESOURCE_PATH+"/images/none.gif' height=16 width=1>"+
req.getContextPath()+ Jenkins.RESOURCE_PATH+"/images/none.gif' height=16 width=1>"+
message+"</div>";
}
@Override public String toString() {
......
/*
* The MIT License
*
* Copyright 2014 Jesse Glick.
*
* 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 jenkins.model;
import hudson.Extension;
import hudson.model.AsyncPeriodicWork;
import hudson.model.DownloadService;
import hudson.model.TaskListener;
import hudson.model.UpdateSite;
import hudson.util.FormValidation;
import java.io.IOException;
import net.sf.json.JSONObject;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
import org.kohsuke.stapler.HttpResponse;
import org.kohsuke.stapler.StaplerRequest;
/**
* Lets user configure how metadata files should be downloaded.
* @see UpdateSite
* @see DownloadService
*/
@Restricted(NoExternalUse.class) // no clear reason for this to be an API
@Extension public final class DownloadSettings extends GlobalConfiguration {
public static DownloadSettings get() {
return Jenkins.getInstance().getInjector().getInstance(DownloadSettings.class);
}
private boolean useBrowser = true; // historical default, not necessarily recommended
public DownloadSettings() {
load();
}
@Override public boolean configure(StaplerRequest req, JSONObject json) throws FormException {
req.bindJSON(this, json);
return true;
}
public boolean isUseBrowser() {
return useBrowser;
}
public void setUseBrowser(boolean useBrowser) {
this.useBrowser = useBrowser;
save();
}
@Extension public static final class DailyCheck extends AsyncPeriodicWork {
public DailyCheck() {
super("Download metadata");
}
@Override public long getRecurrencePeriod() {
return DAY;
}
@Override protected void execute(TaskListener listener) throws IOException, InterruptedException {
if (get().isUseBrowser()) {
return;
}
HttpResponse rsp = Jenkins.getInstance().getPluginManager().doCheckUpdatesServer();
if (rsp instanceof FormValidation) {
listener.error(((FormValidation) rsp).renderHtml());
}
}
}
}
......@@ -96,7 +96,8 @@ THE SOFTWARE.
</tr>
</table>
<div align="right" style="margin-top:1em">
<form method="post" action="checkUpdates">
<j:invokeStatic var="ds" className="jenkins.model.DownloadSettings" method="get"/>
<form method="post" action="${ds.useBrowser ? 'checkUpdates' : 'checkUpdatesServer'}">
${%lastUpdated(app.updateCenter.lastUpdatedString)}
<f:submit value="${%Check now}" />
</form>
......
......@@ -31,6 +31,8 @@ THE SOFTWARE.
-->
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form">
<j:invokeStatic var="ds" className="jenkins.model.DownloadSettings" method="get"/>
<j:if test="${ds.useBrowser}">
<j:forEach var="site" items="${app.updateCenter.sites}">
<j:if test="${site.due or forcedUpdateCheck}">
<script>
......@@ -45,4 +47,5 @@ THE SOFTWARE.
</script>
</j:if>
</j:forEach>
</j:if>
</j:jelly>
......@@ -24,7 +24,7 @@ THE SOFTWARE.
<?jelly escape-by-default='true'?>
<st:compress xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form" xmlns:i="jelly:fmt">
<l:layout title="${it.class.name=='hudson.model.AllView' ? '%Dashboard' : it.viewName}${not empty it.ownerItemGroup.fullDisplayName?' ['+it.ownerItemGroup.fullDisplayName+']':''}">
<l:layout title="${it.class.name=='hudson.model.AllView' ? '%Dashboard' : it.viewName}${not empty it.ownerItemGroup.fullDisplayName?' ['+it.ownerItemGroup.fullDisplayName+']':''}" norefresh="${!it.automaticRefreshEnabled}">
<j:set var="view" value="${it}"/> <!-- expose view to the scripts we include from owner -->
<st:include page="sidepanel.jelly" />
<l:main-panel>
......
package jenkins.security.DownloadSettings
def f = namespace(lib.FormTagLib)
f.section(title: _("Download Preferences")) {
f.entry(title: _("Use Browser"), field: "useBrowser") {
f.checkbox()
}
}
<div>
Check to force the user’s browser to download metadata rather than Jenkins itself doing it.
Actual file downloads (plugins, tools) will still happen from Jenkins itself,
but this can be used to at least <em>see</em> new metadata when Jenkins cannot access the Internet
(but your own browser can, perhaps using some special proxy that Jenkins is not configured to use).
</div>
......@@ -40,7 +40,7 @@ THE SOFTWARE.
<div class="top-sticker noedge">
<div class="top-sticker-inner">
<div id="right-top-nav">
<j:if test="${norefresh==null}">
<j:if test="${norefresh==null or norefresh==false}">
<div id="right-top-nav">
<div class="smallfont">
<j:choose>
......
......@@ -34,7 +34,7 @@ THE SOFTWARE.
Title of the HTML page. Rendered into &lt;title> tag.
</st:attribute>
<st:attribute name="norefresh">
If non-null, auto refresh is disabled on this page.
If non-null and not "false", auto refresh is disabled on this page.
This is necessary for pages that include forms.
</st:attribute>
<st:attribute name="css" deprecated="true">
......@@ -68,8 +68,7 @@ ${h.initPageVariables(context)}
which I suspect can end up creating sessions for wrong resource types (such as static resources.)
-->
<j:set var="_" value="${request.getSession()}"/>
<j:set var="_" value="${h.configureAutoRefresh(request, response, attrs.norefresh!=null)}"/>
<j:set var="_" value="${h.configureAutoRefresh(request, response, attrs.norefresh!=null and !attrs.norefresh.equals(false))}"/>
<j:if test="${request.servletPath=='/' || request.servletPath==''}">
${h.advertiseHeaders(response)}
<j:forEach var="pd" items="${h.pageDecorators}">
......
package hudson.model;
import hudson.model.DownloadService.Downloadable;
import java.io.IOException;
import java.net.URL;
import java.util.Set;
import java.util.TreeSet;
import net.sf.json.JSONObject;
import org.jvnet.hudson.test.Bug;
import org.jvnet.hudson.test.HudsonTestCase;
import org.jvnet.hudson.test.WithoutJenkins;
import org.kohsuke.stapler.StaplerResponse;
import java.io.IOException;
/**
* @author Kohsuke Kawaguchi
*/
......@@ -20,18 +23,15 @@ public class DownloadServiceTest extends HudsonTestCase {
@Override
protected void setUp() throws Exception {
super.setUp();
if (jenkins == null) {
return;
}
// this object receives the submission.
// to bypass the URL restriction, we'll trigger downloadService.download ourselves
job = new Downloadable("test", "UNUSED");
Downloadable.all().add(job);
}
@Override
protected void tearDown() throws Exception {
Downloadable.all().remove(job);
super.tearDown();
}
@Bug(5536)
public void testPost() throws Exception {
// initially it should fail because the data doesn't have a signature
......@@ -59,4 +59,20 @@ public class DownloadServiceTest extends HudsonTestCase {
rsp.setContentType("application/javascript");
rsp.getWriter().println("downloadService.post('test',{'hello':"+hashCode()+"})");
}
@WithoutJenkins // could have been in core/src/test/ but update-center.json was already in test/src/test/ (used by UpdateSiteTest)
public void testLoadJSON() throws Exception {
assertRoots("[list]", "hudson.tasks.Maven.MavenInstaller.json"); // format used by most tools
assertRoots("[data, version]", "hudson.tools.JDKInstaller.json"); // anomalous format
assertRoots("[connectionCheckUrl, core, id, plugins, signature, updateCenterVersion]", "update-center.json");
}
private static void assertRoots(String expected, String file) throws Exception {
URL resource = DownloadServiceTest.class.getResource(file);
assertNotNull(file, resource);
JSONObject json = JSONObject.fromObject(DownloadService.loadJSON(resource));
@SuppressWarnings("unchecked") Set<String> keySet = json.keySet();
assertEquals(expected, new TreeSet<String>(keySet).toString());
}
}
......@@ -26,7 +26,6 @@ package hudson.model;
import com.trilead.ssh2.crypto.Base64;
import hudson.util.TimeUnit2;
import net.sf.json.JSONObject;
import org.apache.commons.io.IOUtils;
import java.io.ByteArrayInputStream;
import java.net.URL;
......@@ -57,8 +56,8 @@ public class UpdateCenterTest {
}
private void doData(String location) throws Exception {
URL url = new URL(location);
String jsonp = IOUtils.toString(url.openStream());
JSONObject json = JSONObject.fromObject(jsonp.substring(jsonp.indexOf('(') + 1, jsonp.lastIndexOf(')')));
String jsonp = DownloadService.loadJSON(url);
JSONObject json = JSONObject.fromObject(jsonp);
UpdateSite us = new UpdateSite("default", url.toExternalForm());
UpdateSite.Data data = us.new Data(json);
......
......@@ -128,10 +128,4 @@ public class UpdateSiteTest {
assertNotNull(us.getPlugin("AdaptivePlugin"));
}
@Test public void updateDirectlyWithHtml() throws Exception {
UpdateSite us = new UpdateSite("default", new URL(baseUrl, "update-center.json.html").toExternalForm());
assertNull(us.getPlugin("AdaptivePlugin"));
assertEquals(FormValidation.ok(), us.updateDirectly(false).get());
assertNotNull(us.getPlugin("AdaptivePlugin"));
}
}
downloadService.post('hudson.tasks.Maven.MavenInstaller',{"list": [
{
"id": "3.1.1",
"name": "3.1.1",
"url": "http://archive.apache.org/dist/maven/binaries/apache-maven-3.1.1-bin.zip"
},
{
"id": "3.1.0",
"name": "3.1.0",
"url": "http://archive.apache.org/dist/maven/binaries/apache-maven-3.1.0-bin.zip"
},
{
"id": "3.0.5",
"name": "3.0.5",
"url": "http://archive.apache.org/dist/maven/binaries/apache-maven-3.0.5-bin.zip"
},
{
"id": "3.0.4",
"name": "3.0.4",
"url": "http://archive.apache.org/dist/maven/binaries/apache-maven-3.0.4-bin.zip"
},
{
"id": "3.0.3",
"name": "3.0.3",
"url": "http://archive.apache.org/dist/maven/binaries/apache-maven-3.0.3-bin.zip"
},
{
"id": "3.0.2",
"name": "3.0.2",
"url": "http://archive.apache.org/dist/maven/binaries/apache-maven-3.0.2-bin.zip"
},
{
"id": "3.0.1",
"name": "3.0.1",
"url": "http://archive.apache.org/dist/maven/binaries/apache-maven-3.0.1-bin.zip"
},
{
"id": "3.0",
"name": "3.0",
"url": "http://archive.apache.org/dist/maven/binaries/apache-maven-3.0-bin.zip"
},
{
"id": "2.2.1",
"name": "2.2.1",
"url": "http://archive.apache.org/dist/maven/binaries/apache-maven-2.2.1-bin.zip"
},
{
"id": "2.2.0",
"name": "2.2.0",
"url": "http://archive.apache.org/dist/maven/binaries/apache-maven-2.2.0-bin.zip"
},
{
"id": "2.1.0",
"name": "2.1.0",
"url": "http://archive.apache.org/dist/maven/binaries/apache-maven-2.1.0-bin.zip"
},
{
"id": "2.0.11",
"name": "2.0.11",
"url": "http://archive.apache.org/dist/maven/binaries/apache-maven-2.0.11-bin.zip"
},
{
"id": "2.0.10",
"name": "2.0.10",
"url": "http://archive.apache.org/dist/maven/binaries/apache-maven-2.0.10-bin.zip"
},
{
"id": "2.0.9",
"name": "2.0.9",
"url": "http://archive.apache.org/dist/maven/binaries/apache-maven-2.0.9-bin.zip"
},
{
"id": "2.0.8",
"name": "2.0.8",
"url": "http://archive.apache.org/dist/maven/binaries/apache-maven-2.0.8-bin.zip"
},
{
"id": "2.0.7",
"name": "2.0.7",
"url": "http://archive.apache.org/dist/maven/binaries/maven-2.0.7-bin.zip"
},
{
"id": "2.0.6",
"name": "2.0.6",
"url": "http://archive.apache.org/dist/maven/binaries/maven-2.0.6-bin.zip"
},
{
"id": "2.0.5",
"name": "2.0.5",
"url": "http://archive.apache.org/dist/maven/binaries/maven-2.0.5-bin.zip"
},
{
"id": "2.0.4",
"name": "2.0.4",
"url": "http://archive.apache.org/dist/maven/binaries/maven-2.0.4-bin.zip"
},
{
"id": "2.0.3",
"name": "2.0.3",
"url": "http://archive.apache.org/dist/maven/binaries/maven-2.0.3-bin.zip"
},
{
"id": "2.0.2",
"name": "2.0.2",
"url": "http://archive.apache.org/dist/maven/binaries/maven-2.0.2-bin.zip"
},
{
"id": "2.0.1",
"name": "2.0.1",
"url": "http://archive.apache.org/dist/maven/binaries/maven-2.0.1-bin.zip"
},
{
"id": "2.0",
"name": "2.0",
"url": "http://archive.apache.org/dist/maven/binaries/maven-2.0-bin.zip"
},
{
"id": "1.1",
"name": "1.1",
"url": "http://archive.apache.org/dist/maven/binaries/maven-1.1.zip"
},
{
"id": "1.0.2",
"name": "1.0.2",
"url": "http://archive.apache.org/dist/maven/binaries/maven-1.0.2.zip"
},
{
"id": "1.0.1",
"name": "1.0.1",
"url": "http://archive.apache.org/dist/maven/binaries/maven-1.0.1.zip"
},
{
"id": "1.0",
"name": "1.0",
"url": "http://archive.apache.org/dist/maven/binaries/maven-1.0.zip"
}
]})
\ No newline at end of file
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册