import io.ktor.server.routing.method
import io.ktor.server.routing.route
import io.ktor.util.AttributeKey
-import kotlinx.html.*
+import kotlinx.html.a
+import kotlinx.html.body
+import kotlinx.html.h1
+import kotlinx.html.head
+import kotlinx.html.li
+import kotlinx.html.title
+import kotlinx.html.ul
import java.net.URI
import java.time.Instant
import java.time.ZoneOffset
resourceProps()
"supportedlock" {
"lockentry" {
- "lockscope" { "shared"() }
+ "lockscope" {
+ "shared"()
+ "exclusive"()
+ }
"locktype" { "write"() }
}
}
respond(HttpStatusCode.NotFound)
}
-suspend fun ApplicationCall.webDavLock(path: StoragePath) {
- beforeWebDav()
+private sealed interface WebDavLockInfo {
+ @JvmInline
+ value class Scalar(val value: String?) : WebDavLockInfo
- if (request.header(HttpHeaders.ContentType) != null)
- receiveText()
+ @JvmInline
+ value class Complex(val subTags: Map<String, WebDavLockInfo>) : WebDavLockInfo
+}
+
+private fun XmlTag.webDavLockInfo(info: WebDavLockInfo, tag: String) {
+ when (info) {
+ is WebDavLockInfo.Scalar -> {
+ info.value?.let {
+ tag { +it }
+ } ?: tag()
+ }
+
+ is WebDavLockInfo.Complex -> {
+ tag {
+ for ((k, v) in info.subTags)
+ webDavLockInfo(v, k)
+ }
+ }
+ }
+}
+
+private fun getXmlTagPrefix(line: String): String {
+ val xmlnsIndex = line.indexOf("xmlns:")
+ if (xmlnsIndex < 0)
+ return ""
+
+ val namespace = line.substring(xmlnsIndex + 6).substringBefore('=')
+ return "$namespace:"
+}
+
+private enum class XmlTagType {
+ SCALAR, OPEN, CLOSE
+}
+
+private fun getXmlTagType(line: String, prefix: String): Triple<String, XmlTagType, String?>? {
+ val (isClose, lineMinusAngleBracket) = if (line.startsWith("</"))
+ Pair(true, line.substring(2))
+ else if (line.startsWith("<"))
+ Pair(false, line.substring(1))
+ else
+ return null
+
+ val lineMinusPrefix = lineMinusAngleBracket.removePrefix(prefix)
+ val tag = lineMinusPrefix.substringBefore('>')
+ if (isClose)
+ return Triple(tag, XmlTagType.CLOSE, null)
+
+ if (tag.endsWith("/"))
+ return Triple(tag.substring(0, tag.length - 1), XmlTagType.SCALAR, null)
+
+ if (lineMinusPrefix.endsWith("</$prefix$tag>"))
+ return Triple(tag, XmlTagType.SCALAR, lineMinusPrefix.substring(tag.length + 1, tag.length + prefix.length + 3))
+ return Triple(tag, XmlTagType.OPEN, null)
+}
+
+private fun <T : Any> Iterator<T>.nextOrNull(): T? = if (hasNext()) next() else null
+
+private fun Iterator<String>.buildLockInfoTree(xmlTagPrefix: String? = null): Pair<Pair<String, WebDavLockInfo>?, Boolean> {
+ val line = nextOrNull()?.trim() ?: return Pair(null, false)
+ if (line.isBlank() || line.startsWith("<?"))
+ return Pair(null, true)
+
+ val prefix = xmlTagPrefix ?: getXmlTagPrefix(line)
+ val (tagName, tagType, tagValue) = getXmlTagType(line, prefix) ?: return Pair(null, true)
+
+ when (tagType) {
+ XmlTagType.SCALAR -> return Pair(Pair(tagName, WebDavLockInfo.Scalar(tagValue)), true)
+ XmlTagType.OPEN -> {
+ val subTrees = buildMap {
+ do {
+ val (subTree, hasNext) = buildLockInfoTree(prefix)
+ subTree?.let { (k, v) -> put(k, v) }
+ } while (hasNext)
+ }
+
+ return Pair(Pair(tagName, WebDavLockInfo.Complex(subTrees)), true)
+ }
+
+ XmlTagType.CLOSE -> return Pair(null, false)
+ }
+}
+
+private val defaultLockInfo: WebDavLockInfo.Complex = WebDavLockInfo.Complex(
+ mapOf(
+ "lockscope" to WebDavLockInfo.Complex(
+ mapOf(
+ "shared" to WebDavLockInfo.Scalar(null)
+ )
+ ),
+ "locktype" to WebDavLockInfo.Complex(
+ mapOf(
+ "write" to WebDavLockInfo.Scalar(null)
+ )
+ ),
+ "owner" to WebDavLockInfo.Scalar(null),
+ )
+)
+
+private const val InfiniteTimeout: String = "Second-31556925000"
+
+private suspend fun ApplicationCall.parseRequestLockInfo(): WebDavLockInfo.Complex {
val depth = request.header(HttpHeaders.Depth) ?: "Infinity"
+ val (requestLockInfoTag, _) = receiveText().lineSequence().iterator().buildLockInfoTree()
+ val requestLockInfo = requestLockInfoTag?.takeIf { it.first == "lockinfo" }?.second as? WebDavLockInfo.Complex ?: defaultLockInfo
+
+ return WebDavLockInfo.Complex(
+ requestLockInfo.subTags + mapOf(
+ "depth" to WebDavLockInfo.Scalar(depth),
+ "timeout" to WebDavLockInfo.Scalar(InfiniteTimeout),
+ "locktoken" to WebDavLockInfo.Complex(
+ mapOf(
+ "href" to WebDavLockInfo.Scalar("opaquelocktoken:${UUID.randomUUID()}")
+ )
+ )
+ )
+ )
+}
+
+suspend fun ApplicationCall.webDavLock(path: StoragePath) {
+ beforeWebDav()
+
+ val info = parseRequestLockInfo()
respondXml {
declaration()
.root("prop", namespace = "DAV:") {
"lockdiscovery" {
- "activelock" {
- "lockscope" { "shared"() }
- "locktype" { "write"() }
- "depth" { +depth }
- "owner"()
- "timeout" { +"Second-86400" }
- "locktoken" {
- "href" { +"opaquelocktoken:${UUID.randomUUID()}" }
- }
- }
+ webDavLockInfo(info, "activeLock")
}
}
}