diff --git a/build.gradle b/build.gradle index b352e04..a303f9a 100644 --- a/build.gradle +++ b/build.gradle @@ -1,47 +1,51 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { - id 'org.springframework.boot' version '3.1.4' - id 'io.spring.dependency-management' version '1.1.3' - id 'org.jetbrains.kotlin.jvm' version '1.8.22' - id 'org.jetbrains.kotlin.plugin.spring' version '1.8.22' - id "org.jetbrains.kotlin.plugin.jpa" version '1.8.22' + id 'org.springframework.boot' version '3.1.4' + id 'io.spring.dependency-management' version '1.1.3' + id 'org.jetbrains.kotlin.jvm' version '1.8.22' + id 'org.jetbrains.kotlin.plugin.spring' version '1.8.22' + id "org.jetbrains.kotlin.plugin.jpa" version '1.8.22' } group = 'net.schowek' version = '0.0.1-SNAPSHOT' java { - sourceCompatibility = '17' + sourceCompatibility = '17' } repositories { - mavenCentral() + mavenCentral() } dependencies { - implementation 'org.springframework.boot:spring-boot-starter-web' - implementation 'com.fasterxml.jackson.module:jackson-module-kotlin' - implementation 'org.jetbrains.kotlin:kotlin-reflect' - implementation 'io.github.microutils:kotlin-logging:3.0.5' + implementation('org.springframework.boot:spring-boot-starter-web') { + exclude group: 'org.springframework.boot', module: 'spring-boot-starter-tomcat' + } + implementation 'org.springframework.boot:spring-boot-starter-undertow' - implementation 'org.springframework.boot:spring-boot-starter-data-jpa' - implementation 'org.mariadb.jdbc:mariadb-java-client:3.2.0' + implementation 'com.fasterxml.jackson.module:jackson-module-kotlin' + implementation 'org.jetbrains.kotlin:kotlin-reflect' + implementation 'io.github.microutils:kotlin-logging:3.0.5' - implementation 'org.jupnp:org.jupnp:2.7.1' - implementation 'org.jupnp:org.jupnp.support:2.7.1' - implementation 'org.apache.httpcomponents:httpclient:4.5.14' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.mariadb.jdbc:mariadb-java-client:3.2.0' - testImplementation 'org.springframework.boot:spring-boot-starter-test' + implementation 'org.jupnp:org.jupnp:2.7.1' + implementation 'org.jupnp:org.jupnp.support:2.7.1' + implementation 'org.apache.httpcomponents:httpclient:4.5.14' + + testImplementation 'org.springframework.boot:spring-boot-starter-test' } tasks.withType(KotlinCompile) { - kotlinOptions { - freeCompilerArgs += '-Xjsr305=strict' - jvmTarget = '17' - } + kotlinOptions { + freeCompilerArgs += '-Xjsr305=strict' + jvmTarget = '17' + } } tasks.named('test') { - useJUnitPlatform() + useJUnitPlatform() } diff --git a/src/main/kotlin/net/schowek/nextclouddlna/NextcloudDLNA.kt b/src/main/kotlin/net/schowek/nextclouddlna/NextcloudDLNA.kt index 0ad6e19..bde07e1 100644 --- a/src/main/kotlin/net/schowek/nextclouddlna/NextcloudDLNA.kt +++ b/src/main/kotlin/net/schowek/nextclouddlna/NextcloudDLNA.kt @@ -8,9 +8,9 @@ import org.springframework.stereotype.Component @Component class NextcloudDLNA( - private val dlnaService: DlnaService + dlnaService: DlnaService ) { - private val upnpService: UpnpService = dlnaService.start() + val upnpService: UpnpService = dlnaService.start() @PreDestroy fun destroy() { diff --git a/src/main/kotlin/net/schowek/nextclouddlna/NextcloudDLNAApp.kt b/src/main/kotlin/net/schowek/nextclouddlna/NextcloudDLNAApp.kt index eaef711..bb0f6e9 100644 --- a/src/main/kotlin/net/schowek/nextclouddlna/NextcloudDLNAApp.kt +++ b/src/main/kotlin/net/schowek/nextclouddlna/NextcloudDLNAApp.kt @@ -2,8 +2,10 @@ package net.schowek.nextclouddlna import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication +import org.springframework.web.servlet.config.annotation.EnableWebMvc @SpringBootApplication +@EnableWebMvc class NextcloudDLNAApp fun main(args: Array) { diff --git a/src/main/kotlin/net/schowek/nextclouddlna/controller/DLNAController.kt b/src/main/kotlin/net/schowek/nextclouddlna/controller/DLNAController.kt new file mode 100644 index 0000000..4b814e0 --- /dev/null +++ b/src/main/kotlin/net/schowek/nextclouddlna/controller/DLNAController.kt @@ -0,0 +1,81 @@ +package net.schowek.nextclouddlna.controller + +import UpnpStreamProcessor +import jakarta.servlet.http.HttpServletRequest +import mu.KLogging +import net.schowek.nextclouddlna.NextcloudDLNA +import net.schowek.nextclouddlna.dlna.media.MediaServer +import org.springframework.core.io.InputStreamResource +import org.springframework.core.io.Resource +import org.springframework.http.HttpHeaders +import org.springframework.http.HttpStatusCode +import org.springframework.http.MediaType.APPLICATION_OCTET_STREAM_VALUE +import org.springframework.http.ResponseEntity +import org.springframework.stereotype.Controller +import org.springframework.util.MultiValueMap +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestMethod.* +import org.springframework.web.bind.annotation.ResponseBody + + +@Controller +class DLNAController( + private val dlna: NextcloudDLNA, + private val streamRequestMapper: StreamRequestMapper +) { + @RequestMapping( + method = [GET, HEAD], value = ["/dev/{uid}/icon.png"], + produces = [APPLICATION_OCTET_STREAM_VALUE] + ) + @ResponseBody + fun handleGetIcon( + @PathVariable("uid") uid: String, + request: HttpServletRequest + ): Resource { + logger.info { "GET ICON request from ${request.remoteAddr}: ${request.requestURI}" } + return InputStreamResource(MediaServer.iconResource()); + } + + @RequestMapping( + method = [GET, HEAD], value = [ + "/dev/{uid}/desc", + "/dev/{uid}/svc/upnp-org/ContentDirectory/desc", + "/dev/{uid}/svc/upnp-org/ConnectionManager/desc" + ], + produces = ["application/xml;charset=utf8", "text/xml;charset=utf8"] + ) + fun handleGet( + @PathVariable("uid") uid: String, + request: HttpServletRequest + ): ResponseEntity { + logger.info { "GET request from ${request.remoteAddr}: ${request.requestURI}" } + val r = UpnpStreamProcessor(dlna).processMessage(streamRequestMapper.map(request)) + return ResponseEntity( + r.body, + HttpHeaders().also { h -> r.headers.entries.forEach { h.add(it.key, it.value.joinToString { it }) } }, + HttpStatusCode.valueOf(r.operation.statusCode) + ) + } + + @RequestMapping( + method = [POST], value = [ + "/dev/{uid}/svc/upnp-org/ContentDirectory/action" + ], + produces = ["application/xml;charset=utf8", "text/xml;charset=utf8"] + ) + fun handlePost( + @PathVariable("uid") uid: String, + request: HttpServletRequest + ): ResponseEntity { + logger.info { "POST request from ${request.remoteAddr}: ${request.requestURI}" } + val r = UpnpStreamProcessor(dlna).processMessage(streamRequestMapper.map(request)) + return ResponseEntity( + r.body, + HttpHeaders().also { h -> r.headers.entries.forEach { h.add(it.key, it.value.joinToString { it }) } }, + HttpStatusCode.valueOf(r.operation.statusCode) + ) + } + + companion object : KLogging() +} diff --git a/src/main/kotlin/net/schowek/nextclouddlna/controller/ErrorHandler.kt b/src/main/kotlin/net/schowek/nextclouddlna/controller/ErrorHandler.kt new file mode 100644 index 0000000..d8d8b39 --- /dev/null +++ b/src/main/kotlin/net/schowek/nextclouddlna/controller/ErrorHandler.kt @@ -0,0 +1,26 @@ +package net.schowek.nextclouddlna.controller + +import mu.KLogging +import org.springframework.http.HttpStatus.NOT_FOUND +import org.springframework.web.bind.annotation.ControllerAdvice +import org.springframework.web.bind.annotation.ExceptionHandler +import org.springframework.web.bind.annotation.ResponseBody +import org.springframework.web.bind.annotation.ResponseStatus +import org.springframework.web.context.request.WebRequest +import org.springframework.web.servlet.NoHandlerFoundException + +@ControllerAdvice +class ErrorHandler { + @ExceptionHandler(NoHandlerFoundException::class) + @ResponseStatus(NOT_FOUND) + @ResponseBody + fun handleNoHandlerFound(e: NoHandlerFoundException, request: WebRequest): HashMap { + val response = HashMap() + response["status"] = "fail" + response["message"] = e.localizedMessage + logger.info { "404 from $request (${e.requestURL})" } + return response + } + + companion object : KLogging() +} \ No newline at end of file diff --git a/src/main/kotlin/net/schowek/nextclouddlna/controller/StreamRequestMapper.kt b/src/main/kotlin/net/schowek/nextclouddlna/controller/StreamRequestMapper.kt new file mode 100644 index 0000000..4524fd8 --- /dev/null +++ b/src/main/kotlin/net/schowek/nextclouddlna/controller/StreamRequestMapper.kt @@ -0,0 +1,79 @@ +package net.schowek.nextclouddlna.controller + +import jakarta.servlet.http.HttpServletRequest +import mu.KLogging +import org.jupnp.model.message.* +import org.springframework.stereotype.Component +import java.net.InetAddress +import java.net.URI + +@Component +class StreamRequestMapper { + fun map(request: HttpServletRequest): StreamRequestMessage { + val requestMessage = StreamRequestMessage( + UpnpRequest.Method.getByHttpName(request.method), + URI(request.requestURI) + // TODO put request.inputStream.readBytes() here + ) + if (requestMessage.operation.method == UpnpRequest.Method.UNKNOWN) { + logger.warn("Method not supported by UPnP stack: {}", request.method) + throw RuntimeException("Method not supported: {}" + request.method) + } + + requestMessage.connection = MyHttpServerConnection(request) + requestMessage.headers = createHeaders(request) + setBody(request, requestMessage) + return requestMessage + } + + private fun setBody( + request: HttpServletRequest, + requestMessage: StreamRequestMessage + ) { + val bodyBytes = request.inputStream.readBytes() + + logger.debug(" Reading request body bytes: " + bodyBytes.size) + if (bodyBytes.isNotEmpty() && requestMessage.isContentTypeMissingOrText) { + logger.debug("Request contains textual entity body, converting then setting string on message") + requestMessage.setBodyCharacters(bodyBytes) + } else if (bodyBytes.isNotEmpty()) { + logger.debug("Request contains binary entity body, setting bytes on message") + requestMessage.setBody(UpnpMessage.BodyType.BYTES, bodyBytes) + } else { + logger.debug("Request did not contain entity body") + } + } + + private fun createHeaders(request: HttpServletRequest): UpnpHeaders { + val headers = mutableMapOf>() + with(request.headerNames) { + if (this != null) { + while (hasMoreElements()) { + with(nextElement()) { + headers[this] = listOf(request.getHeader(this)) + } + } + } + } + return UpnpHeaders(headers) + } + + inner class MyHttpServerConnection( + private val request: HttpServletRequest + ) : Connection { + override fun isOpen(): Boolean { + return true + } + + override fun getRemoteAddress(): InetAddress? { + return if (request.remoteAddr != null) InetAddress.getByName(request.remoteAddr) else null + } + + override fun getLocalAddress(): InetAddress? { + return if (request.localAddr != null) InetAddress.getByName(request.localAddr) else null + } + } + + companion object : KLogging() +} + diff --git a/src/main/kotlin/net/schowek/nextclouddlna/controller/UpnpStreamProcessor.kt b/src/main/kotlin/net/schowek/nextclouddlna/controller/UpnpStreamProcessor.kt new file mode 100644 index 0000000..ee602ed --- /dev/null +++ b/src/main/kotlin/net/schowek/nextclouddlna/controller/UpnpStreamProcessor.kt @@ -0,0 +1,26 @@ +import mu.KLogging +import net.schowek.nextclouddlna.NextcloudDLNA +import org.jupnp.model.message.StreamRequestMessage +import org.jupnp.model.message.StreamResponseMessage +import org.jupnp.model.message.UpnpResponse +import org.jupnp.transport.spi.UpnpStream + + +class UpnpStreamProcessor( + dlna: NextcloudDLNA +) : UpnpStream(dlna.upnpService.protocolFactory) { + + fun processMessage(requestMsg: StreamRequestMessage): StreamResponseMessage { + logger.debug { "Processing $requestMsg" } + var response = super.process(requestMsg) + if (response == null) { + response = StreamResponseMessage(UpnpResponse.Status.NOT_FOUND) + } + return response + } + + override fun run() { + } + + companion object : KLogging() +} \ No newline at end of file diff --git a/src/main/kotlin/net/schowek/nextclouddlna/dlna/DllnaService.kt b/src/main/kotlin/net/schowek/nextclouddlna/dlna/DllnaService.kt index aeb9edc..27bd6a1 100644 --- a/src/main/kotlin/net/schowek/nextclouddlna/dlna/DllnaService.kt +++ b/src/main/kotlin/net/schowek/nextclouddlna/dlna/DllnaService.kt @@ -1,5 +1,6 @@ package net.schowek.nextclouddlna.dlna +import mu.KLogging import net.schowek.nextclouddlna.dlna.media.MediaServer import net.schowek.nextclouddlna.dlna.transport.ApacheStreamClient import net.schowek.nextclouddlna.dlna.transport.ApacheStreamClientConfiguration @@ -7,9 +8,13 @@ import net.schowek.nextclouddlna.dlna.transport.MyStreamServerConfiguration import net.schowek.nextclouddlna.dlna.transport.MyStreamServerImpl import net.schowek.nextclouddlna.util.ServerInfoProvider import org.jupnp.DefaultUpnpServiceConfiguration +import org.jupnp.UpnpService import org.jupnp.UpnpServiceConfiguration import org.jupnp.UpnpServiceImpl +import org.jupnp.model.meta.LocalDevice import org.jupnp.protocol.ProtocolFactory +import org.jupnp.protocol.ProtocolFactoryImpl +import org.jupnp.protocol.async.SendingNotificationAlive import org.jupnp.registry.Registry import org.jupnp.transport.impl.NetworkAddressFactoryImpl import org.jupnp.transport.spi.NetworkAddressFactory @@ -39,9 +44,13 @@ class DlnaService( override fun createRegistry(pf: ProtocolFactory): Registry { return RegistryImplWithOverrides(this) } + + override fun createProtocolFactory(): ProtocolFactory? { + return MyProtocolFactory(this) + } } - private inner class MyUpnpServiceConfiguration : DefaultUpnpServiceConfiguration(8081) { + private inner class MyUpnpServiceConfiguration : DefaultUpnpServiceConfiguration(8080) { override fun createStreamClient(): StreamClient<*> { return ApacheStreamClient( ApacheStreamClientConfiguration(syncProtocolExecutorService) @@ -72,3 +81,14 @@ class DlnaService( } } + +class MyProtocolFactory( + upnpService: UpnpService +) : ProtocolFactoryImpl(upnpService) { + override fun createSendingNotificationAlive(localDevice: LocalDevice): SendingNotificationAlive { + logger.info { "SENDING ALIVE $localDevice" } + return SendingNotificationAlive(upnpService, localDevice) + } + + companion object : KLogging() +} \ No newline at end of file diff --git a/src/main/kotlin/net/schowek/nextclouddlna/dlna/media/MediaServer.kt b/src/main/kotlin/net/schowek/nextclouddlna/dlna/media/MediaServer.kt index e34c4ee..a313e88 100644 --- a/src/main/kotlin/net/schowek/nextclouddlna/dlna/media/MediaServer.kt +++ b/src/main/kotlin/net/schowek/nextclouddlna/dlna/media/MediaServer.kt @@ -10,6 +10,7 @@ import org.springframework.beans.factory.annotation.Qualifier import org.springframework.beans.factory.annotation.Value import org.springframework.stereotype.Component import java.io.IOException +import java.io.InputStream @Component @@ -20,17 +21,17 @@ class MediaServer( private val connectionManagerService: LocalService<*>, @Value("\${server.friendlyName}") private val friendlyName: String, - private val externalUrls: ExternalUrls + externalUrls: ExternalUrls ) { - val device = LocalDevice( - DeviceIdentity(uniqueSystemIdentifier("DLNAtoad-MediaServer"), 300), + final val device = LocalDevice( + DeviceIdentity(uniqueSystemIdentifier("Nextcloud-DLNA-MediaServer"), 300), UDADeviceType(DEVICE_TYPE, VERSION), DeviceDetails(friendlyName, externalUrls.selfURI), - createDeviceIcon(), arrayOf(contentDirectoryService, connectionManagerService) + createDeviceIcon(), + arrayOf(contentDirectoryService, connectionManagerService) ) - @PostConstruct - fun init() { + init { logger.info("uniqueSystemIdentifier: {} ({})", device.identity.udn, friendlyName) } @@ -41,14 +42,18 @@ class MediaServer( @Throws(IOException::class) fun createDeviceIcon(): Icon { - val resource = MediaServer::class.java.getResourceAsStream("/$ICON_FILENAME") - ?: throw IllegalStateException("Icon not found.") + val resource = iconResource() return resource.use { res -> Icon("image/png", 48, 48, 8, ICON_FILENAME, res).also { it.validate() } } } + + fun iconResource(): InputStream { + return MediaServer::class.java.getResourceAsStream("/$ICON_FILENAME") + ?: throw IllegalStateException("Icon not found.") + } } } diff --git a/src/main/kotlin/net/schowek/nextclouddlna/dlna/transport/MyHttpExchangeUpnpStream.kt b/src/main/kotlin/net/schowek/nextclouddlna/dlna/transport/MyHttpExchangeUpnpStream.kt deleted file mode 100644 index 1755cd0..0000000 --- a/src/main/kotlin/net/schowek/nextclouddlna/dlna/transport/MyHttpExchangeUpnpStream.kt +++ /dev/null @@ -1,117 +0,0 @@ -package net.schowek.nextclouddlna.dlna.transport - -import com.sun.net.httpserver.HttpExchange -import mu.KLogging -import org.jupnp.model.message.* -import org.jupnp.protocol.ProtocolFactory -import org.jupnp.transport.spi.UpnpStream -import org.jupnp.util.io.IO -import java.io.IOException -import java.io.InputStream -import java.io.OutputStream -import java.net.HttpURLConnection - - -abstract class MyHttpExchangeUpnpStream( - protocolFactory: ProtocolFactory, - private val httpExchange: HttpExchange -) : UpnpStream(protocolFactory) { - - override fun run() { - try { - // Status - val requestMessage = StreamRequestMessage( - UpnpRequest.Method.getByHttpName(httpExchange.requestMethod), - httpExchange.requestURI - ) - if (requestMessage.operation.method == UpnpRequest.Method.UNKNOWN) { - logger.warn("Method not supported by UPnP stack: {}", httpExchange.requestMethod) - throw RuntimeException("Method not supported: {}" + httpExchange.requestMethod) - } - - // Protocol - requestMessage.operation.httpMinorVersion = if (httpExchange.protocol.uppercase() == "HTTP/1.1") 1 else 0 - // Connection wrapper - requestMessage.connection = createConnection() - // Headers - requestMessage.headers = UpnpHeaders(httpExchange.requestHeaders) - - // Body - val bodyBytes: ByteArray - var inputStream: InputStream? = null - try { - inputStream = httpExchange.requestBody - bodyBytes = IO.readBytes(inputStream) - } finally { - inputStream?.close() - } - logger.info(" Reading request body bytes: " + bodyBytes.size) - if (bodyBytes.isNotEmpty() && requestMessage.isContentTypeMissingOrText) { - logger.debug("Request contains textual entity body, converting then setting string on message") - requestMessage.setBodyCharacters(bodyBytes) - } else if (bodyBytes.isNotEmpty()) { - logger.debug("Request contains binary entity body, setting bytes on message") - requestMessage.setBody(UpnpMessage.BodyType.BYTES, bodyBytes) - } else { - logger.debug("Request did not contain entity body") - } - if (bodyBytes.isNotEmpty()) { - logger.debug(" Request body: " + requestMessage.body) - } - val responseMessage = process(requestMessage) - - // Return the response - if (responseMessage != null) { - // Headers - httpExchange.responseHeaders.putAll(responseMessage.headers) - - // Body - val responseBodyBytes = if (responseMessage.hasBody()) responseMessage.bodyBytes else null - val contentLength = responseBodyBytes?.size ?: -1 - logger.info("Sending HTTP response message: $responseMessage with content length: $contentLength") - httpExchange.sendResponseHeaders(responseMessage.operation.statusCode, contentLength.toLong()) - if (responseBodyBytes!!.isNotEmpty()) { - logger.debug(" Response body: " + responseMessage.body) - } - if (contentLength > 0) { - logger.debug("Response message has body, writing bytes to stream...") - var outputStream: OutputStream? = null - try { - outputStream = httpExchange.responseBody - IO.writeBytes(outputStream, responseBodyBytes) - outputStream.flush() - } finally { - outputStream?.close() - } - } - } else { - // If it's null, it's 404, everything else needs a proper httpResponse - logger.info("Sending HTTP response status: " + HttpURLConnection.HTTP_NOT_FOUND) - httpExchange.sendResponseHeaders(HttpURLConnection.HTTP_NOT_FOUND, -1) - } - responseSent(responseMessage) - } catch (t: Throwable) { - // You definitely want to catch all Exceptions here, otherwise the server will - // simply close the socket and you get an "unexpected end of file" on the client. - // The same is true if you just rethrow an IOException - it is a mystery why it - // is declared then on the HttpHandler interface if it isn't handled in any - // way... so we always do error handling here. - - // TODO: We should only send an error if the problem was on our side - // You don't have to catch Throwable unless, like we do here in unit tests, - // you might run into Errors as well (assertions). - logger.warn("Exception occurred during UPnP stream processing:", t) - try { - httpExchange.sendResponseHeaders(HttpURLConnection.HTTP_INTERNAL_ERROR, -1) - } catch (ex: IOException) { - logger.warn("Couldn't send error response: ", ex) - } - responseException(t) - } - } - - protected abstract fun createConnection(): Connection? - - companion object: KLogging() -} - diff --git a/src/main/kotlin/net/schowek/nextclouddlna/dlna/transport/MyStreamServerImpl.kt b/src/main/kotlin/net/schowek/nextclouddlna/dlna/transport/MyStreamServerImpl.kt index a8c3a79..ac0123c 100644 --- a/src/main/kotlin/net/schowek/nextclouddlna/dlna/transport/MyStreamServerImpl.kt +++ b/src/main/kotlin/net/schowek/nextclouddlna/dlna/transport/MyStreamServerImpl.kt @@ -1,104 +1,37 @@ package net.schowek.nextclouddlna.dlna.transport import com.sun.net.httpserver.HttpExchange -import com.sun.net.httpserver.HttpHandler -import com.sun.net.httpserver.HttpServer import mu.KLogging -import org.jupnp.model.message.Connection import org.jupnp.transport.Router -import org.jupnp.transport.spi.InitializationException import org.jupnp.transport.spi.StreamServer -import java.io.IOException import java.net.InetAddress -import java.net.InetSocketAddress class MyStreamServerImpl( private val configuration: MyStreamServerConfiguration ) : StreamServer { - private var server: HttpServer? = null - - @Synchronized - @Throws(InitializationException::class) override fun init(bindAddress: InetAddress, router: Router) { - try { - val socketAddress = InetSocketAddress(bindAddress, configuration.listenPort) - server = HttpServer.create(socketAddress, configuration.tcpConnectionBacklog) - server!!.createContext("/", MyRequestHttpHandler(router)) - logger.info("* * * * * * * * * * * * * * * * * * * * * * * * * * * * * *") - logger.info("Created server (for receiving TCP streams) on: " + server!!.address) - logger.info("* * * * * * * * * * * * * * * * * * * * * * * * * * * * * *") - } catch (ex: Exception) { - throw InitializationException("Could not initialize " + javaClass.simpleName + ": " + ex.toString(), ex) - } } - @Synchronized override fun getPort(): Int { - return server!!.address.port + return configuration.listenPort; } override fun getConfiguration(): MyStreamServerConfiguration { return configuration } - @Synchronized override fun run() { - logger.info("Starting StreamServer...") - // Starts a new thread but inherits the properties of the calling thread - server!!.start() } - @Synchronized override fun stop() { - logger.info("Stopping StreamServer...") - if (server != null) { - server!!.stop(1) - } } - inner class MyRequestHttpHandler(private val router: Router) : HttpHandler { - // This is executed in the request receiving thread! - @Throws(IOException::class) - override fun handle(httpExchange: HttpExchange) { - // And we pass control to the service, which will (hopefully) start a new thread immediately so we can - // continue the receiving thread ASAP - logger.info("Received HTTP exchange: " + httpExchange.requestMethod + " " + httpExchange.requestURI + " from " + httpExchange.remoteAddress) - router.received( - object : MyHttpExchangeUpnpStream(router.protocolFactory, httpExchange) { - override fun createConnection(): Connection { - return MyHttpServerConnection(httpExchange) - } - } - ) - } - } - - /** - * Logs a warning and returns `true`, we can't access the socket using the awful JDK webserver API. - * Override this method if you know how to do it. - */ private fun isConnectionOpen(exchange: HttpExchange?): Boolean { logger.warn("Can't check client connection, socket access impossible on JDK webserver!") return true } - private inner class MyHttpServerConnection( - private val exchange: HttpExchange - ) : Connection { - override fun isOpen(): Boolean { - return isConnectionOpen(exchange) - } - - override fun getRemoteAddress(): InetAddress? { - return if (exchange.remoteAddress != null) exchange.remoteAddress.address else null - } - - override fun getLocalAddress(): InetAddress? { - return if (exchange.localAddress != null) exchange.localAddress.address else null - } - } - companion object: KLogging() }