no jetty needed

This commit is contained in:
xis 2023-10-10 17:56:45 +02:00
parent 2b212b9607
commit b165fd7c67
12 changed files with 299 additions and 115 deletions

View file

@ -10,7 +10,7 @@ import org.springframework.stereotype.Component
class NextcloudDLNA(
private val dlnaService: DlnaService
) {
private val upnpService: UpnpService = dlnaService.start()
val upnpService: UpnpService = dlnaService.start()
@PreDestroy
fun destroy() {

View file

@ -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<String>) {

View file

@ -0,0 +1,74 @@
package net.schowek.nextclouddlna.controller
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import mu.KLogging
import net.schowek.nextclouddlna.NextcloudDLNA
import org.jupnp.model.message.Connection
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.stereotype.Controller
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestMethod.GET
import org.springframework.web.bind.annotation.RequestMethod.HEAD
import org.springframework.web.bind.annotation.RequestMethod.POST
import org.springframework.web.bind.annotation.ResponseBody
import java.net.InetAddress
@Controller
class DLNAController(
private val dlna: NextcloudDLNA
) {
@RequestMapping(
method = [GET, HEAD], value = [
"/dev/{uid}/desc",
"/dev/{uid}/svc/upnp-org/ContentDirectory/desc",
"/dev/{uid}/svc/upnp-org/ConnectionManager/desc"
]
)
@ResponseBody
fun handleGet(
@PathVariable("uid") uid: String,
request: HttpServletRequest,
response: HttpServletResponse
): ResponseEntity<String> {
logger.info { "GET request from ${request.remoteAddr}: ${request.requestURI}" }
MyUpnpStream(dlna.upnpService.protocolFactory, request, response).run()
return ResponseEntity(HttpStatus.OK);
}
@RequestMapping(
method = [POST], value = [
"/dev/{uid}/svc/upnp-org/ContentDirectory/action"
]
)
@ResponseBody
fun handlePost(
@PathVariable("uid") uid: String,
request: HttpServletRequest,
response: HttpServletResponse
): ResponseEntity<String> {
logger.info { "POST request from ${request.remoteAddr}: ${request.requestURI}" }
MyUpnpStream(dlna.upnpService.protocolFactory, request, response).run()
return ResponseEntity(HttpStatus.OK);
}
companion object : KLogging()
}
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
}
}

View file

@ -0,0 +1,27 @@
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 ExceptionResolver {
@ExceptionHandler(NoHandlerFoundException::class)
@ResponseStatus(NOT_FOUND)
@ResponseBody
fun handleNoHandlerFound(e: NoHandlerFoundException, request: WebRequest): HashMap<String, String> {
val response = HashMap<String, String>()
response["status"] = "fail"
response["message"] = e.localizedMessage
logger.info { "404 from $request (${e.requestURL})" }
return response
}
companion object : KLogging()
}

View file

@ -0,0 +1,133 @@
package net.schowek.nextclouddlna.controller
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import mu.KLogging
import org.jupnp.model.message.StreamRequestMessage
import org.jupnp.model.message.UpnpHeaders
import org.jupnp.model.message.UpnpMessage
import org.jupnp.model.message.UpnpRequest
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
import java.net.URI
class MyUpnpStream(
protocolFactory: ProtocolFactory,
private val request: HttpServletRequest,
private val response: HttpServletResponse
) : UpnpStream(protocolFactory) {
override fun run() = try {
// Status
val requestMessage = StreamRequestMessage(
UpnpRequest.Method.getByHttpName(request.method),
URI(request.requestURI)
)
if (requestMessage.operation.method == UpnpRequest.Method.UNKNOWN) {
logger.warn("Method not supported by UPnP stack: {}", request.method)
throw RuntimeException("Method not supported: {}" + request.method)
}
// Protocol
requestMessage.operation.httpMinorVersion = 1
// Connection wrapper
requestMessage.connection = MyHttpServerConnection(request)
// Headers
val headers = mutableMapOf<String, List<String>>()
val headerNames = request.headerNames
if (headerNames != null) {
while (headerNames.hasMoreElements()) {
val headerName = headerNames.nextElement()
headers[headerName] = listOf(request.getHeader(headerName))
}
}
requestMessage.headers = UpnpHeaders(headers)
// Body
val bodyBytes: ByteArray
var inputStream: InputStream? = null
try {
inputStream = request.inputStream
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
responseMessage.headers.forEach { response.addHeader(it.key, it.value.joinToString { v -> v }) }
// 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")
response.status = responseMessage.operation.statusCode
response.setContentLength(contentLength)
// TODO return ResponseEntity
// httpExchange.sendResponseHeaders(responseMessage.operation.statusCode, contentLength.toLong())
if (responseBodyBytes!!.isNotEmpty()) {
logger.info { " Response body: ${responseMessage.body}" }
}
if (contentLength > 0) {
logger.debug("Response message has body, writing bytes to stream...")
var outputStream: OutputStream? = null
try {
outputStream = response.outputStream
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)
// TODO return ResponseEntity
// 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 {
// TODO return ResponseEntity
// httpExchange.sendResponseHeaders(HttpURLConnection.HTTP_INTERNAL_ERROR, -1)
} catch (ex: IOException) {
logger.warn("Couldn't send error response: ", ex)
}
responseException(t)
}
companion object : KLogging()
}

View file

@ -10,6 +10,7 @@ import org.jupnp.DefaultUpnpServiceConfiguration
import org.jupnp.UpnpServiceConfiguration
import org.jupnp.UpnpServiceImpl
import org.jupnp.protocol.ProtocolFactory
import org.jupnp.protocol.ProtocolFactoryImpl
import org.jupnp.registry.Registry
import org.jupnp.transport.impl.NetworkAddressFactoryImpl
import org.jupnp.transport.spi.NetworkAddressFactory
@ -23,12 +24,10 @@ import java.net.NetworkInterface
@Component
class DlnaService(
private val serverInfoProvider: ServerInfoProvider,
private val upnpServiceConfiguration: MyUpnpServiceConfiguration,
private val mediaServer: MediaServer
) {
// Named this way cos NetworkAddressFactoryImpl has a bindAddresses field.
private val addressesToBind: List<InetAddress> = listOf(serverInfoProvider.address!!)
fun start() = MyUpnpService(MyUpnpServiceConfiguration()).also {
fun start() = MyUpnpService(upnpServiceConfiguration).also {
it.startup()
it.registry.addDevice(mediaServer.device)
}
@ -39,35 +38,8 @@ class DlnaService(
override fun createRegistry(pf: ProtocolFactory): Registry {
return RegistryImplWithOverrides(this)
}
}
private inner class MyUpnpServiceConfiguration : DefaultUpnpServiceConfiguration(8081) {
override fun createStreamClient(): StreamClient<*> {
return ApacheStreamClient(
ApacheStreamClientConfiguration(syncProtocolExecutorService)
)
}
override fun createStreamServer(networkAddressFactory: NetworkAddressFactory): StreamServer<*> {
return MyStreamServerImpl(
MyStreamServerConfiguration(networkAddressFactory.streamListenPort)
)
}
override fun createNetworkAddressFactory(
streamListenPort: Int,
multicastResponsePort: Int
): NetworkAddressFactory {
return MyNetworkAddressFactory(streamListenPort, multicastResponsePort)
}
}
inner class MyNetworkAddressFactory(
streamListenPort: Int,
multicastResponsePort: Int
) : NetworkAddressFactoryImpl(streamListenPort, multicastResponsePort) {
override fun isUsableAddress(iface: NetworkInterface, address: InetAddress): Boolean {
return addressesToBind.contains(address)
override fun createProtocolFactory(): ProtocolFactory? {
return MyProtocolFactory(this)
}
}
}

View file

@ -0,0 +1,17 @@
package net.schowek.nextclouddlna.dlna
import mu.KLogging
import org.jupnp.UpnpService
import org.jupnp.model.meta.LocalDevice
import org.jupnp.protocol.ProtocolFactoryImpl
import org.jupnp.protocol.async.SendingNotificationAlive
class MyProtocolFactory(
upnpService: UpnpService
) : ProtocolFactoryImpl(upnpService) {
override fun createSendingNotificationAlive(localDevice: LocalDevice): SendingNotificationAlive {
logger.info { "SENDING ALIVE $localDevice" }
return SendingNotificationAlive(upnpService, localDevice)
}
companion object : KLogging()
}

View file

@ -0,0 +1,30 @@
package net.schowek.nextclouddlna.dlna
import net.schowek.nextclouddlna.dlna.transport.ApacheStreamClient
import net.schowek.nextclouddlna.dlna.transport.ApacheStreamClientConfiguration
import net.schowek.nextclouddlna.dlna.transport.MyStreamServerConfiguration
import net.schowek.nextclouddlna.dlna.transport.MyStreamServerImpl
import org.jupnp.DefaultUpnpServiceConfiguration
import org.jupnp.transport.spi.NetworkAddressFactory
import org.jupnp.transport.spi.StreamClient
import org.jupnp.transport.spi.StreamServer
import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Component
@Component
class MyUpnpServiceConfiguration(
@Value("\${server.port}")
streamListenPort: Int
) : DefaultUpnpServiceConfiguration(streamListenPort) {
override fun createStreamClient(): StreamClient<*> {
return ApacheStreamClient(
ApacheStreamClientConfiguration(syncProtocolExecutorService)
)
}
override fun createStreamServer(networkAddressFactory: NetworkAddressFactory): StreamServer<*> {
return MyStreamServerImpl(
MyStreamServerConfiguration(networkAddressFactory.streamListenPort)
)
}
}

View file

@ -23,14 +23,13 @@ class MediaServer(
private val externalUrls: ExternalUrls
) {
val device = LocalDevice(
DeviceIdentity(uniqueSystemIdentifier("DLNAtoad-MediaServer"), 300),
DeviceIdentity(uniqueSystemIdentifier("Nextcloud-DLNA-MediaServer"), 300),
UDADeviceType(DEVICE_TYPE, VERSION),
DeviceDetails(friendlyName, externalUrls.selfURI),
createDeviceIcon(), arrayOf(contentDirectoryService, connectionManagerService)
)
@PostConstruct
fun init() {
init {
logger.info("uniqueSystemIdentifier: {} ({})", device.identity.udn, friendlyName)
}

View file

@ -5,7 +5,6 @@ import org.jupnp.transport.spi.StreamServerConfiguration
class MyStreamServerConfiguration(
private val listenPort: Int
) : StreamServerConfiguration {
val tcpConnectionBacklog = 0
override fun getListenPort(): Int {
return listenPort
}

View file

@ -1,104 +1,28 @@
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<MyStreamServerConfiguration> {
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()
}

View file

@ -1,4 +1,7 @@
server:
error:
whitelabel:
enabled: false
port: ${NEXTCLOUD_DLNA_SERVER_PORT:8080}
interface: ${NEXTCLOUD_DLNA_INTERFACE:eth0}
friendlyName: ${NEXTCLOUD_DLNA_FRIENDLY_NAME:Nextcloud-DLNA}
@ -15,3 +18,7 @@ spring:
jpa:
hibernate:
ddl-auto: none
mvc:
throw-exception-if-no-handler-found: true
resources:
add-mappings: false