c0341849338362eff5f7e3cbbea357c085c4b70a
[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.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
13 import java.io.*
14 import java.math.BigInteger
15 import java.nio.file.Files
16 import java.nio.file.attribute.PosixFilePermissions
17 import java.security.MessageDigest
18 import java.util.*
19 import java.util.zip.ZipEntry
20 import java.util.zip.ZipFile
21 import kotlin.collections.HashMap
22
23
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()
28
29   class State {
30     val mapping = HashMap<String, String>()
31   }
32
33   override fun getState(): State? {
34     return myState
35   }
36
37   override fun loadState(state: State) {
38     myState.mapping.putAll(state.mapping)
39   }
40
41   companion object {
42     @JvmStatic
43     fun getInstance(): MavenWrapperMapping {
44       return ServiceManager.getService(MavenWrapperMapping::class.java)
45     }
46   }
47 }
48
49 class MavenWrapperSupport {
50
51   private val myMapping = MavenWrapperMapping.getInstance()
52   val DISTS_DIR = "wrapper/dists"
53
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)
61       }
62       else {
63         myMapping.myState.mapping.remove(urlString)
64       }
65     }
66
67
68     val zipFile = getZipFile(urlString)
69     if (!zipFile.isFile) {
70       val partFile = File(zipFile.parentFile, "${zipFile.name}.part-${System.currentTimeMillis()}")
71       HttpRequests.request(urlString)
72         .forceHttps(true)
73         .connectTimeout(30_000)
74         .readTimeout(30_000)
75         .saveToFile(partFile, null) //todo: cancel and progress
76       FileUtil.rename(partFile, zipFile)
77     }
78     if (!zipFile.isFile) {
79       throw RuntimeException(SyncBundle.message("cannot.download.zip.from", urlString))
80     }
81     val home = unpackZipFile(zipFile).canonicalFile
82     myMapping.myState.mapping[urlString] = home.absolutePath
83     return MavenDistribution(home, urlString)
84
85   }
86
87
88   private fun unpackZipFile(zipFile: File): File {
89     unzip(zipFile)
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))
94     }
95     val mavenHome = dirs[0]
96     if (!SystemInfo.isWindows) {
97       makeMavenBinRunnable(mavenHome)
98     }
99     return mavenHome
100   }
101
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)
106   }
107
108   private fun unzip(zip: File) {
109     val unpackDir = zip.parentFile
110     val destinationCanonicalPath = unpackDir.canonicalPath
111     var errorUnpacking = false
112     try {
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)) {
120             FileUtil.delete(zip)
121             throw RuntimeException("Directory traversal attack detected, zip file is malicious and IDEA dropped it")
122           }
123
124           if (entry.isDirectory) {
125             destFile.mkdirs()
126           }
127           else {
128             destFile.parentFile.mkdirs()
129             BufferedOutputStream(FileOutputStream(destFile)).use {
130               StreamUtil.copy(zipFile.getInputStream(entry), it)
131             }
132           }
133         }
134
135       }
136       errorUnpacking = false
137     }
138     finally {
139       if (errorUnpacking) {
140         zip.parentFile.listFiles { it -> it.name != zip.name }?.forEach { FileUtil.delete(it) }
141       }
142     }
143
144   }
145
146
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)
153
154     return File(File(File(distsDir, distName), md5Hash), baseName).absoluteFile
155   }
156
157   private fun getDistName(distUrl: String): String {
158     val p = distUrl.lastIndexOf("/")
159     return if (p < 0) distUrl else distUrl.substring(p + 1)
160   }
161
162   private fun getMd5Hash(string: String): String {
163     return try {
164       val messageDigest = MessageDigest.getInstance("MD5")
165       val bytes = string.toByteArray()
166       messageDigest.update(bytes)
167       BigInteger(1, messageDigest.digest()).toString(32)
168     }
169     catch (var4: Exception) {
170       throw RuntimeException("Could not hash input string.", var4)
171     }
172   }
173
174
175   companion object {
176     @JvmStatic
177     fun hasWrapperConfigured(baseDir: VirtualFile): Boolean {
178       return !getWrapperDistributionUrl(baseDir).isNullOrEmpty()
179     }
180
181     @JvmStatic
182     fun getWrapperDistributionUrl(baseDir: VirtualFile?): String? {
183       val wrapperProperties = baseDir?.findChild(".mvn")?.findChild("wrapper")?.findChild("maven-wrapper.properties") ?: return null
184
185       val properties = Properties()
186
187       val stream = ByteArrayInputStream(wrapperProperties.contentsToByteArray(true))
188       properties.load(stream)
189       return properties.getProperty("distributionUrl")
190     }
191   }
192 }