457e39c19f5b079a9350ef2a71bebc0df86755be
[idea/community.git] / platform / built-in-server / src / org / jetbrains / builtInWebServer / StaticFileHandler.kt
1 package org.jetbrains.builtInWebServer
2
3 import com.intellij.openapi.project.Project
4 import com.intellij.util.PathUtilRt
5 import io.netty.buffer.ByteBufUtf8Writer
6 import io.netty.channel.Channel
7 import io.netty.channel.ChannelFutureListener
8 import io.netty.handler.codec.http.*
9 import io.netty.handler.stream.ChunkedStream
10 import org.jetbrains.builtInWebServer.ssi.SsiExternalResolver
11 import org.jetbrains.builtInWebServer.ssi.SsiProcessor
12 import org.jetbrains.io.FileResponses
13 import org.jetbrains.io.addKeepAliveIfNeed
14 import java.nio.file.Files
15 import java.nio.file.Path
16 import java.nio.file.Paths
17
18 private class StaticFileHandler : WebServerFileHandler() {
19   override val pageFileExtensions = arrayOf("html", "htm", "shtml", "stm", "shtm")
20
21   private var ssiProcessor: SsiProcessor? = null
22
23   override fun process(pathInfo: PathInfo, canonicalPath: CharSequence, project: Project, request: FullHttpRequest, channel: Channel, projectNameIfNotCustomHost: String?, extraHeaders: HttpHeaders): Boolean {
24     if (pathInfo.ioFile != null || pathInfo.file!!.isInLocalFileSystem) {
25       val ioFile = pathInfo.ioFile ?: Paths.get(pathInfo.file!!.path)
26       val nameSequence = ioFile.fileName.toString()
27       //noinspection SpellCheckingInspection
28       if (nameSequence.endsWith(".shtml", true) || nameSequence.endsWith(".stm", true) || nameSequence.endsWith(".shtm", true)) {
29         processSsi(ioFile, PathUtilRt.getParentPath(canonicalPath.toString()), project, request, channel, extraHeaders)
30         return true
31       }
32
33       FileResponses.sendFile(request, channel, ioFile, extraHeaders)
34     }
35     else {
36       val file = pathInfo.file!!
37       val response = FileResponses.prepareSend(request, channel, file.timeStamp, file.name, extraHeaders) ?: return true
38
39       val keepAlive = response.addKeepAliveIfNeed(request)
40       if (request.method() != HttpMethod.HEAD) {
41         HttpUtil.setContentLength(response, file.length)
42       }
43
44       channel.write(response)
45
46       if (request.method() != HttpMethod.HEAD) {
47         channel.write(ChunkedStream(file.inputStream))
48       }
49
50       val future = channel.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT)
51       if (!keepAlive) {
52         future.addListener(ChannelFutureListener.CLOSE)
53       }
54     }
55
56     return true
57   }
58
59   private fun processSsi(file: Path, path: String, project: Project, request: FullHttpRequest, channel: Channel, extraHeaders: HttpHeaders) {
60     if (ssiProcessor == null) {
61       ssiProcessor = SsiProcessor(false)
62     }
63
64     val buffer = channel.alloc().ioBuffer()
65     val keepAlive: Boolean
66     var releaseBuffer = true
67     try {
68       val lastModified = ssiProcessor!!.process(SsiExternalResolver(project, request, path, file.parent), file, ByteBufUtf8Writer(buffer))
69       val response = FileResponses.prepareSend(request, channel, lastModified, file.fileName.toString(), extraHeaders) ?: return
70       keepAlive = response.addKeepAliveIfNeed(request)
71       if (request.method() != HttpMethod.HEAD) {
72         HttpUtil.setContentLength(response, buffer.readableBytes().toLong())
73       }
74
75       channel.write(response)
76
77       if (request.method() != HttpMethod.HEAD) {
78         releaseBuffer = false
79         channel.write(buffer)
80       }
81     }
82     finally {
83       if (releaseBuffer) {
84         buffer.release()
85       }
86     }
87
88     val future = channel.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT)
89     if (!keepAlive) {
90       future.addListener(ChannelFutureListener.CLOSE)
91     }
92   }
93 }
94
95 internal fun checkAccess(file: Path, root: Path = file.root): Boolean {
96   var parent = file
97   do {
98     if (!hasAccess(parent)) {
99       return false
100     }
101     parent = parent.parent ?: break
102   }
103   while (parent != root)
104   return true
105 }
106
107 // deny access to any dot prefixed file
108 private fun hasAccess(result: Path) = Files.isReadable(result) && !(Files.isHidden(result) || result.fileName.toString().startsWith('.'))