1 // Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
2 package org.jetbrains.idea.maven.server
4 import com.intellij.openapi.components.*
5 import com.intellij.openapi.progress.ProgressIndicator
6 import com.intellij.openapi.util.SystemInfo
7 import com.intellij.openapi.util.io.FileUtil
8 import com.intellij.openapi.util.io.StreamUtil
9 import com.intellij.openapi.vfs.VirtualFile
10 import com.intellij.util.io.HttpRequests
11 import org.jetbrains.idea.maven.execution.SyncBundle
12 import org.jetbrains.idea.maven.utils.MavenLog
13 import org.jetbrains.idea.maven.utils.MavenUtil
15 import java.math.BigInteger
16 import java.nio.file.Files
17 import java.nio.file.attribute.PosixFilePermissions
18 import java.security.MessageDigest
20 import java.util.zip.ZipEntry
21 import java.util.zip.ZipFile
22 import kotlin.collections.HashMap
25 @State(name = "MavenWrapperMapping",
26 storages = [Storage(value = "maven.wrapper.mapping.xml", roamingType = RoamingType.PER_OS)])
27 class MavenWrapperMapping : PersistentStateComponent<MavenWrapperMapping.State> {
28 internal var myState = State()
31 val mapping = HashMap<String, String>()
34 override fun getState(): State? {
38 override fun loadState(state: State) {
39 myState.mapping.putAll(state.mapping)
44 fun getInstance(): MavenWrapperMapping {
45 return ServiceManager.getService(MavenWrapperMapping::class.java)
50 class MavenWrapperSupport {
52 private val myMapping = MavenWrapperMapping.getInstance()
53 val DISTS_DIR = "wrapper/dists"
55 @Throws(IOException::class)
56 fun downloadAndInstallMaven(urlString: String, indicator: ProgressIndicator?): MavenDistribution {
57 val cachedHome = myMapping.myState.mapping.get(urlString)
58 if (cachedHome != null) {
59 val file = File(cachedHome)
60 if (file.isDirectory) {
61 return MavenDistribution(file, urlString)
64 myMapping.myState.mapping.remove(urlString)
68 val zipFile = getZipFile(urlString)
69 if (!zipFile.isFile) {
70 val partFile = File(zipFile.parentFile, "${zipFile.name}.part-${System.currentTimeMillis()}")
71 indicator?.apply { text = SyncBundle.message("maven.sync.wrapper.dowloading.from", urlString) }
72 HttpRequests.request(urlString)
74 .connectTimeout(30_000)
76 .saveToFile(partFile, indicator)
77 FileUtil.rename(partFile, zipFile)
79 if (!zipFile.isFile) {
80 throw RuntimeException(SyncBundle.message("cannot.download.zip.from", urlString))
82 val home = unpackZipFile(zipFile, indicator).canonicalFile
83 myMapping.myState.mapping[urlString] = home.absolutePath
84 return MavenDistribution(home, urlString)
89 private fun unpackZipFile(zipFile: File, indicator: ProgressIndicator?): File {
90 unzip(zipFile, indicator)
91 val dirs = zipFile.parentFile.listFiles { it -> it.isDirectory }
92 if (dirs == null || dirs.size != 1) {
93 MavenLog.LOG.warn("Expected exactly 1 top level dir in Maven distribution, found: " + dirs?.asList())
94 throw IllegalStateException(SyncBundle.message("zip.is.not.correct", zipFile.absoluteFile))
96 val mavenHome = dirs[0]
97 if (!SystemInfo.isWindows) {
98 makeMavenBinRunnable(mavenHome)
103 private fun makeMavenBinRunnable(mavenHome: File?) {
104 val mvnExe = File(mavenHome, "bin/mvn").canonicalFile
105 val permissions = PosixFilePermissions.fromString("rwxr-xr-x")
106 Files.setPosixFilePermissions(mvnExe.toPath(), permissions)
109 private fun unzip(zip: File, indicator: ProgressIndicator?) {
110 indicator?.apply { text = SyncBundle.message("maven.sync.wrapper.unpacking") }
111 val unpackDir = zip.parentFile
112 val destinationCanonicalPath = unpackDir.canonicalPath
113 var errorUnpacking = false
115 ZipFile(zip).use { zipFile ->
116 val entries: Enumeration<*> = zipFile.entries()
117 while (entries.hasMoreElements()) {
118 val entry = entries.nextElement() as ZipEntry
119 val destFile = File(unpackDir, entry.name)
120 val canonicalPath = destFile.canonicalPath
121 if (!canonicalPath.startsWith(destinationCanonicalPath)) {
123 throw RuntimeException("Directory traversal attack detected, zip file is malicious and IDEA dropped it")
126 if (entry.isDirectory) {
130 destFile.parentFile.mkdirs()
131 BufferedOutputStream(FileOutputStream(destFile)).use {
132 StreamUtil.copy(zipFile.getInputStream(entry), it)
138 errorUnpacking = false
139 indicator?.apply { text = SyncBundle.message("maven.sync.wrapper.unpacked.into", destinationCanonicalPath) }
142 if (errorUnpacking) {
143 indicator?.apply { text = SyncBundle.message("maven.sync.wrapper.failure") }
144 zip.parentFile.listFiles { it -> it.name != zip.name }?.forEach { FileUtil.delete(it) }
151 fun getZipFile(distributionUrl: String): File {
152 val baseName: String = getDistName(distributionUrl)
153 val distName: String = FileUtil.getNameWithoutExtension(baseName)
154 val md5Hash: String = getMd5Hash(distributionUrl)
155 val m2dir = MavenUtil.resolveM2Dir()
156 val distsDir = File(m2dir, DISTS_DIR)
158 return File(File(File(distsDir, distName), md5Hash), baseName).absoluteFile
161 private fun getDistName(distUrl: String): String {
162 val p = distUrl.lastIndexOf("/")
163 return if (p < 0) distUrl else distUrl.substring(p + 1)
166 private fun getMd5Hash(string: String): String {
168 val messageDigest = MessageDigest.getInstance("MD5")
169 val bytes = string.toByteArray()
170 messageDigest.update(bytes)
171 BigInteger(1, messageDigest.digest()).toString(32)
173 catch (var4: Exception) {
174 throw RuntimeException("Could not hash input string.", var4)
181 fun hasWrapperConfigured(baseDir: VirtualFile): Boolean {
182 return !getWrapperDistributionUrl(baseDir).isNullOrEmpty()
186 fun getWrapperDistributionUrl(baseDir: VirtualFile?): String? {
187 val wrapperProperties = baseDir?.findChild(".mvn")?.findChild("wrapper")?.findChild("maven-wrapper.properties") ?: return null
189 val properties = Properties()
191 val stream = ByteArrayInputStream(wrapperProperties.contentsToByteArray(true))
192 properties.load(stream)
193 return properties.getProperty("distributionUrl")