IDEA-246851 - Add action to disable maven wrapper
[idea/community.git] / plugins / maven / src / main / java / org / jetbrains / idea / maven / server / MavenWrapperSupport.kt
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
3
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
14 import java.io.*
15 import java.math.BigInteger
16 import java.nio.file.Files
17 import java.nio.file.attribute.PosixFilePermissions
18 import java.security.MessageDigest
19 import java.util.*
20 import java.util.zip.ZipEntry
21 import java.util.zip.ZipFile
22 import kotlin.collections.HashMap
23
24
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()
29
30   class State {
31     val mapping = HashMap<String, String>()
32   }
33
34   override fun getState(): State? {
35     return myState
36   }
37
38   override fun loadState(state: State) {
39     myState.mapping.putAll(state.mapping)
40   }
41
42   companion object {
43     @JvmStatic
44     fun getInstance(): MavenWrapperMapping {
45       return ServiceManager.getService(MavenWrapperMapping::class.java)
46     }
47   }
48 }
49
50 class MavenWrapperSupport {
51
52   private val myMapping = MavenWrapperMapping.getInstance()
53   val DISTS_DIR = "wrapper/dists"
54
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)
62       }
63       else {
64         myMapping.myState.mapping.remove(urlString)
65       }
66     }
67
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)
73         .forceHttps(true)
74         .connectTimeout(30_000)
75         .readTimeout(30_000)
76         .saveToFile(partFile, indicator)
77       FileUtil.rename(partFile, zipFile)
78     }
79     if (!zipFile.isFile) {
80       throw RuntimeException(SyncBundle.message("cannot.download.zip.from", urlString))
81     }
82     val home = unpackZipFile(zipFile, indicator).canonicalFile
83     myMapping.myState.mapping[urlString] = home.absolutePath
84     return MavenDistribution(home, urlString)
85
86   }
87
88
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))
95     }
96     val mavenHome = dirs[0]
97     if (!SystemInfo.isWindows) {
98       makeMavenBinRunnable(mavenHome)
99     }
100     return mavenHome
101   }
102
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)
107   }
108
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
114     try {
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)) {
122             FileUtil.delete(zip)
123             throw RuntimeException("Directory traversal attack detected, zip file is malicious and IDEA dropped it")
124           }
125
126           if (entry.isDirectory) {
127             destFile.mkdirs()
128           }
129           else {
130             destFile.parentFile.mkdirs()
131             BufferedOutputStream(FileOutputStream(destFile)).use {
132               StreamUtil.copy(zipFile.getInputStream(entry), it)
133             }
134           }
135         }
136
137       }
138       errorUnpacking = false
139       indicator?.apply { text = SyncBundle.message("maven.sync.wrapper.unpacked.into", destinationCanonicalPath) }
140     }
141     finally {
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) }
145       }
146     }
147
148   }
149
150
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)
157
158     return File(File(File(distsDir, distName), md5Hash), baseName).absoluteFile
159   }
160
161   private fun getDistName(distUrl: String): String {
162     val p = distUrl.lastIndexOf("/")
163     return if (p < 0) distUrl else distUrl.substring(p + 1)
164   }
165
166   private fun getMd5Hash(string: String): String {
167     return try {
168       val messageDigest = MessageDigest.getInstance("MD5")
169       val bytes = string.toByteArray()
170       messageDigest.update(bytes)
171       BigInteger(1, messageDigest.digest()).toString(32)
172     }
173     catch (var4: Exception) {
174       throw RuntimeException("Could not hash input string.", var4)
175     }
176   }
177
178
179   companion object {
180     @JvmStatic
181     fun hasWrapperConfigured(baseDir: VirtualFile): Boolean {
182       return !getWrapperDistributionUrl(baseDir).isNullOrEmpty()
183     }
184
185     @JvmStatic
186     fun getWrapperDistributionUrl(baseDir: VirtualFile?): String? {
187       val wrapperProperties = baseDir?.findChild(".mvn")?.findChild("wrapper")?.findChild("maven-wrapper.properties") ?: return null
188
189       val properties = Properties()
190
191       val stream = ByteArrayInputStream(wrapperProperties.contentsToByteArray(true))
192       properties.load(stream)
193       return properties.getProperty("distributionUrl")
194     }
195   }
196 }