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.util.SystemInfo
6 import com.intellij.openapi.util.io.FileUtil
7 import com.intellij.openapi.util.io.StreamUtil
8 import com.intellij.openapi.vfs.VirtualFile
9 import com.intellij.util.io.HttpRequests
10 import org.jetbrains.idea.maven.execution.SyncBundle
11 import org.jetbrains.idea.maven.utils.MavenLog
12 import org.jetbrains.idea.maven.utils.MavenUtil
14 import java.math.BigInteger
15 import java.nio.file.Files
16 import java.nio.file.attribute.PosixFilePermissions
17 import java.security.MessageDigest
19 import java.util.zip.ZipEntry
20 import java.util.zip.ZipFile
21 import kotlin.collections.HashMap
24 @State(name = "MavenWrapperMapping",
25 storages = [Storage(value = "maven.wrapper.mapping.xml", roamingType = RoamingType.PER_OS)])
26 class MavenWrapperMapping : PersistentStateComponent<MavenWrapperMapping.State> {
27 internal var myState = State()
30 val mapping = HashMap<String, String>()
33 override fun getState(): State? {
37 override fun loadState(state: State) {
38 myState.mapping.putAll(state.mapping)
43 fun getInstance(): MavenWrapperMapping {
44 return ServiceManager.getService(MavenWrapperMapping::class.java)
49 class MavenWrapperSupport {
51 private val myMapping = MavenWrapperMapping.getInstance()
52 val DISTS_DIR = "wrapper/dists"
54 @Throws(IOException::class)
55 fun downloadAndInstallMaven(urlString: String): MavenDistribution {
56 val cachedHome = myMapping.myState.mapping.get(urlString)
57 if (cachedHome != null) {
58 val file = File(cachedHome)
59 if (file.isDirectory) {
60 return MavenDistribution(file, urlString)
63 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 HttpRequests.request(urlString)
73 .connectTimeout(30_000)
75 .saveToFile(partFile, null) //todo: cancel and progress
76 FileUtil.rename(partFile, zipFile)
78 if (!zipFile.isFile) {
79 throw RuntimeException(SyncBundle.message("cannot.download.zip.from", urlString))
81 val home = unpackZipFile(zipFile).canonicalFile
82 myMapping.myState.mapping[urlString] = home.absolutePath
83 return MavenDistribution(home, urlString)
88 private fun unpackZipFile(zipFile: File): File {
90 val dirs = zipFile.parentFile.listFiles { it -> it.isDirectory }
91 if (dirs == null || dirs.size != 1) {
92 MavenLog.LOG.warn("Expected exactly 1 top level dir in Maven distribution, found: " + dirs?.asList())
93 throw IllegalStateException(SyncBundle.message("zip.is.not.correct", zipFile.absoluteFile))
95 val mavenHome = dirs[0]
96 if (!SystemInfo.isWindows) {
97 makeMavenBinRunnable(mavenHome)
102 private fun makeMavenBinRunnable(mavenHome: File?) {
103 val mvnExe = File(mavenHome, "bin/mvn").canonicalFile
104 val permissions = PosixFilePermissions.fromString("rwxr-xr-x")
105 Files.setPosixFilePermissions(mvnExe.toPath(), permissions)
108 private fun unzip(zip: File) {
109 val unpackDir = zip.parentFile
110 val destinationCanonicalPath = unpackDir.canonicalPath
111 var errorUnpacking = false
113 ZipFile(zip).use { zipFile ->
114 val entries: Enumeration<*> = zipFile.entries()
115 while (entries.hasMoreElements()) {
116 val entry = entries.nextElement() as ZipEntry
117 val destFile = File(unpackDir, entry.name)
118 val canonicalPath = destFile.canonicalPath
119 if (!canonicalPath.startsWith(destinationCanonicalPath)) {
121 throw RuntimeException("Directory traversal attack detected, zip file is malicious and IDEA dropped it")
124 if (entry.isDirectory) {
128 destFile.parentFile.mkdirs()
129 BufferedOutputStream(FileOutputStream(destFile)).use {
130 StreamUtil.copy(zipFile.getInputStream(entry), it)
136 errorUnpacking = false
139 if (errorUnpacking) {
140 zip.parentFile.listFiles { it -> it.name != zip.name }?.forEach { FileUtil.delete(it) }
147 fun getZipFile(distributionUrl: String): File {
148 val baseName: String = getDistName(distributionUrl)
149 val distName: String = FileUtil.getNameWithoutExtension(baseName)
150 val md5Hash: String = getMd5Hash(distributionUrl)
151 val m2dir = MavenUtil.resolveM2Dir()
152 val distsDir = File(m2dir, DISTS_DIR)
154 return File(File(File(distsDir, distName), md5Hash), baseName).absoluteFile
157 private fun getDistName(distUrl: String): String {
158 val p = distUrl.lastIndexOf("/")
159 return if (p < 0) distUrl else distUrl.substring(p + 1)
162 private fun getMd5Hash(string: String): String {
164 val messageDigest = MessageDigest.getInstance("MD5")
165 val bytes = string.toByteArray()
166 messageDigest.update(bytes)
167 BigInteger(1, messageDigest.digest()).toString(32)
169 catch (var4: Exception) {
170 throw RuntimeException("Could not hash input string.", var4)
177 fun hasWrapperConfigured(baseDir: VirtualFile): Boolean {
178 return !getWrapperDistributionUrl(baseDir).isNullOrEmpty()
182 fun getWrapperDistributionUrl(baseDir: VirtualFile?): String? {
183 val wrapperProperties = baseDir?.findChild(".mvn")?.findChild("wrapper")?.findChild("maven-wrapper.properties") ?: return null
185 val properties = Properties()
187 val stream = ByteArrayInputStream(wrapperProperties.contentsToByteArray(true))
188 properties.load(stream)
189 return properties.getProperty("distributionUrl")