Merge remote-tracking branch 'origin/upnpservice_as_bean' into dev

This commit is contained in:
xis 2023-10-22 12:57:16 +02:00
commit 615b488a49
22 changed files with 249 additions and 119 deletions

View file

@ -46,6 +46,7 @@ dependencies {
implementation 'org.jupnp:org.jupnp:2.7.1'
implementation 'org.jupnp:org.jupnp.support:2.7.1'
implementation 'org.osgi:org.osgi.service.http:1.2.2'
implementation 'org.apache.httpcomponents:httpclient:4.5.14'
// to avoid snakeyaml-1.3 vulnerability CVE-2022-1471
implementation 'org.yaml:snakeyaml:2.2'

View file

@ -1,7 +1,7 @@
package net.schowek.nextclouddlna.controller
import net.schowek.nextclouddlna.dlna.media.MediaServer
import net.schowek.nextclouddlna.dlna.DlnaService
import net.schowek.nextclouddlna.dlna.MediaServer
import org.jupnp.support.model.DIDLObject
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.http.HttpStatus
@ -14,11 +14,14 @@ class UpnpControllerIntTest extends UpnpAwareSpecification {
@Autowired
private MediaServer mediaServer
@Autowired
private DlnaService dlnaService
def uid
def setup() {
uid = mediaServer.serviceIdentifier
dlnaService.start()
}
def "should serve icon"() {

View file

@ -0,0 +1,55 @@
package net.schowek.nextclouddlna.dlna
import org.jupnp.UpnpService
import org.jupnp.model.message.discovery.OutgoingSearchRequest
import org.jupnp.model.message.header.HostHeader
import org.jupnp.model.message.header.MANHeader
import org.jupnp.model.message.header.STAllHeader
import org.jupnp.model.message.header.UpnpHeader
import org.jupnp.model.types.HostPort
import org.springframework.beans.factory.annotation.Autowired
import support.IntegrationSpecification
import support.beans.dlna.upnp.UpnpServiceConfigurationInt
import static org.jupnp.model.Constants.IPV4_UPNP_MULTICAST_GROUP
import static org.jupnp.model.Constants.UPNP_MULTICAST_PORT
import static org.jupnp.model.message.UpnpRequest.Method.MSEARCH
import static org.jupnp.model.message.header.UpnpHeader.Type.*
import static org.jupnp.model.types.NotificationSubtype.ALL
import static org.jupnp.model.types.NotificationSubtype.DISCOVER
class DlnaServiceIntTest extends IntegrationSpecification {
@Autowired
private UpnpService upnpService
@Autowired
private MediaServer mediaServer
def "should send initial multicast Upnp datagrams on start"() {
given:
def configuration = upnpService.configuration as UpnpServiceConfigurationInt
def sut = new DlnaService(upnpService, mediaServer)
expect:
configuration.outgoingDatagramMessages == []
when:
sut.start()
then:
configuration.outgoingDatagramMessages.any()
configuration.outgoingDatagramMessages[0].class == OutgoingSearchRequest
with(configuration.outgoingDatagramMessages[0] as OutgoingSearchRequest) {
assert it.operation.method == MSEARCH
assert it.destinationAddress == InetAddress.getByName(IPV4_UPNP_MULTICAST_GROUP)
assert it.destinationPort == UPNP_MULTICAST_PORT
assert header(it, MAN, MANHeader.class) == DISCOVER.headerString
assert header(it, ST, STAllHeader.class).headerString == ALL.headerString
assert header(it, HOST, HostHeader.class) == new HostPort(IPV4_UPNP_MULTICAST_GROUP, UPNP_MULTICAST_PORT)
}
}
def <T> T header(OutgoingSearchRequest request, UpnpHeader.Type type, Class<? extends UpnpHeader<T>> clazz) {
return clazz.cast(request.headers.get(type).find()).value
}
}

View file

@ -6,15 +6,21 @@ import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootContextLoader
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.web.client.TestRestTemplate
import org.springframework.context.annotation.Import
import org.springframework.test.annotation.DirtiesContext
import org.springframework.test.context.ActiveProfiles
import org.springframework.test.context.ContextConfiguration
import spock.lang.Specification
import support.beans.TestConfig
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.DEFINED_PORT
import static org.springframework.test.annotation.DirtiesContext.ClassMode.*
@ContextConfiguration(loader = SpringBootContextLoader, classes = NextcloudDLNAApp.class)
@SpringBootTest(webEnvironment = DEFINED_PORT)
@ActiveProfiles("integration")
@Import(TestConfig.class)
@DirtiesContext(classMode = AFTER_CLASS)
class IntegrationSpecification extends Specification {
@Autowired
private TestRestTemplate restTemplate
@ -27,6 +33,6 @@ class IntegrationSpecification extends Specification {
private ServerInfoProvider serverInfoProvider
protected String urlWithPort(String uri = "") {
return "http://localhost:" + serverInfoProvider.port + uri;
return "http://" + serverInfoProvider.host + ":" + serverInfoProvider.port + uri;
}
}

View file

@ -0,0 +1,7 @@
package support.beans
import org.springframework.context.annotation.ComponentScan
@ComponentScan(["support", "net.schowek.nextclouddlna"])
class TestConfig {
}

View file

@ -0,0 +1,51 @@
package support.beans.dlna.upnp
import org.jupnp.DefaultUpnpServiceConfiguration
import org.jupnp.model.message.OutgoingDatagramMessage
import org.jupnp.transport.impl.DatagramIOConfigurationImpl
import org.jupnp.transport.impl.DatagramIOImpl
import org.jupnp.transport.spi.DatagramIO
import org.jupnp.transport.spi.NetworkAddressFactory
import org.jupnp.transport.spi.StreamClient
import org.jupnp.transport.spi.StreamServer
import org.springframework.context.annotation.Profile
import org.springframework.stereotype.Component
@Component
@Profile("integration")
class UpnpServiceConfigurationInt extends DefaultUpnpServiceConfiguration {
List<OutgoingDatagramMessage> outgoingDatagramMessages = new ArrayList<>()
@Override
public StreamClient createStreamClient() {
return null
}
@Override
public StreamServer createStreamServer(NetworkAddressFactory networkAddressFactory) {
return null
}
@Override
public DatagramIO createDatagramIO(NetworkAddressFactory networkAddressFactory) {
return new MockDatagramIO(this, new DatagramIOConfigurationImpl())
}
private void onOutgoingDatagramMessage(OutgoingDatagramMessage message) {
outgoingDatagramMessages.add(message)
}
class MockDatagramIO extends DatagramIOImpl {
private final UpnpServiceConfigurationInt upnpServiceConfiguration
MockDatagramIO(UpnpServiceConfigurationInt upnpServiceConfiguration, DatagramIOConfigurationImpl configuration) {
super(configuration)
this.upnpServiceConfiguration = upnpServiceConfiguration
}
@Override
void send(OutgoingDatagramMessage message) {
upnpServiceConfiguration.onOutgoingDatagramMessage(message)
}
}
}

View file

@ -1,6 +1,6 @@
package net.schowek.nextclouddlna.nextcloud.config
package support.beans.nextcloud.config
import net.schowek.nextclouddlna.nextcloud.config.NextcloudAppPathProvider
import org.springframework.context.annotation.Profile
import org.springframework.stereotype.Component

View file

@ -1,5 +1,6 @@
package net.schowek.nextclouddlna.util
package support.beans.util
import net.schowek.nextclouddlna.util.ServerInfoProvider
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.annotation.Profile
import org.springframework.stereotype.Component

View file

@ -1,4 +1,4 @@
package net.schowek.nextclouddlna.util
package support.beans.util
import org.springframework.beans.factory.annotation.Value
import org.springframework.boot.web.server.ConfigurableWebServerFactory

View file

@ -1,90 +0,0 @@
package net.schowek.nextclouddlna
import jakarta.annotation.PreDestroy
import mu.KLogging
import net.schowek.nextclouddlna.dlna.media.MediaServer
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 net.schowek.nextclouddlna.util.ServerInfoProvider
import org.jupnp.DefaultUpnpServiceConfiguration
import org.jupnp.UpnpServiceConfiguration
import org.jupnp.UpnpServiceImpl
import org.jupnp.model.message.StreamRequestMessage
import org.jupnp.model.message.StreamResponseMessage
import org.jupnp.model.message.UpnpResponse
import org.jupnp.protocol.ProtocolFactory
import org.jupnp.registry.RegistryImpl
import org.jupnp.transport.impl.NetworkAddressFactoryImpl
import org.jupnp.transport.spi.NetworkAddressFactory
import org.springframework.context.event.ContextRefreshedEvent
import org.springframework.context.event.EventListener
import org.springframework.stereotype.Component
import java.net.InetAddress
import java.net.NetworkInterface
@Component
class DlnaService(
private val mediaServer: MediaServer,
private val serverInfoProvider: ServerInfoProvider,
) {
private val addressesToBind: List<String> = listOf(serverInfoProvider.host)
var upnpService = MyUpnpService(MyUpnpServiceConfiguration())
fun start() {
upnpService.startup()
upnpService.registry.addDevice(mediaServer.device)
}
@EventListener
fun handleContextRefresh(event: ContextRefreshedEvent) {
start()
}
@PreDestroy
fun destroy() {
upnpService.shutdown()
}
fun processRequest(requestMsg: StreamRequestMessage): StreamResponseMessage {
logger.debug { "Processing $requestMsg" }
return with(upnpService.protocolFactory.createReceivingSync(requestMsg)) {
run()
outputMessage
?: StreamResponseMessage(UpnpResponse.Status.NOT_FOUND).also {
logger.warn { "Could not get response for ${requestMsg.operation.method} ${requestMsg}" }
}
}.also {
logger.debug { "Response: ${it.operation.statusCode} ${it.body}" }
}
}
inner class MyUpnpService(
configuration: UpnpServiceConfiguration
) : UpnpServiceImpl(configuration) {
override fun createRegistry(pf: ProtocolFactory) = RegistryImpl(this)
}
private inner class MyUpnpServiceConfiguration : DefaultUpnpServiceConfiguration(serverInfoProvider.port) {
override fun createStreamClient() =
ApacheStreamClient(ApacheStreamClientConfiguration(syncProtocolExecutorService))
override fun createStreamServer(networkAddressFactory: NetworkAddressFactory) =
MyStreamServerImpl(MyStreamServerConfiguration(networkAddressFactory.streamListenPort))
override fun createNetworkAddressFactory(streamListenPort: Int, multicastResponsePort: Int) =
MyNetworkAddressFactory(streamListenPort, multicastResponsePort)
}
inner class MyNetworkAddressFactory(
streamListenPort: Int,
multicastResponsePort: Int
) : NetworkAddressFactoryImpl(streamListenPort, multicastResponsePort) {
override fun isUsableAddress(iface: NetworkInterface, address: InetAddress) =
addressesToBind.contains(address.hostAddress)
}
companion object : KLogging()
}

View file

@ -2,9 +2,9 @@ package net.schowek.nextclouddlna.controller
import jakarta.servlet.http.HttpServletRequest
import mu.KLogging
import net.schowek.nextclouddlna.DlnaService
import net.schowek.nextclouddlna.dlna.DlnaService
import net.schowek.nextclouddlna.dlna.StreamMessageMapper
import net.schowek.nextclouddlna.dlna.media.MediaServer
import net.schowek.nextclouddlna.dlna.MediaServer
import org.springframework.core.io.InputStreamResource
import org.springframework.core.io.Resource
import org.springframework.http.MediaType.APPLICATION_OCTET_STREAM_VALUE

View file

@ -0,0 +1,48 @@
package net.schowek.nextclouddlna.dlna
import jakarta.annotation.PreDestroy
import mu.KLogging
import org.jupnp.UpnpService
import org.jupnp.model.message.StreamRequestMessage
import org.jupnp.model.message.StreamResponseMessage
import org.jupnp.model.message.UpnpResponse
import org.springframework.context.event.ContextRefreshedEvent
import org.springframework.context.event.EventListener
import org.springframework.stereotype.Component
@Component
class DlnaService(
private val upnpService: UpnpService,
private val mediaServer: MediaServer
) {
fun start() {
upnpService.startup()
upnpService.registry.addDevice(mediaServer.device)
}
@EventListener(condition = "!@environment.acceptsProfiles('integration')")
fun handleContextRefresh(event: ContextRefreshedEvent) {
start()
}
@PreDestroy
fun destroy() {
upnpService.shutdown()
}
fun processRequest(requestMsg: StreamRequestMessage): StreamResponseMessage {
logger.debug { "Processing $requestMsg" }
return with(upnpService.protocolFactory.createReceivingSync(requestMsg)) {
run()
outputMessage
?: StreamResponseMessage(UpnpResponse.Status.NOT_FOUND).also {
logger.warn { "Could not get response for ${requestMsg.operation.method} ${requestMsg}" }
}
}.also {
logger.debug { "Response: ${it.operation.statusCode} ${it.body}" }
}
}
companion object : KLogging()
}

View file

@ -1,4 +1,4 @@
package net.schowek.nextclouddlna.dlna.media
package net.schowek.nextclouddlna.dlna
import mu.KLogging
import net.schowek.nextclouddlna.util.ExternalUrls

View file

@ -0,0 +1,10 @@
package net.schowek.nextclouddlna.dlna.upnp
import org.jupnp.UpnpServiceConfiguration
import org.jupnp.UpnpServiceImpl
import org.springframework.stereotype.Component
@Component
class MyUpnpService(
upnpServiceConfiguration: UpnpServiceConfiguration
) : UpnpServiceImpl(upnpServiceConfiguration)

View file

@ -0,0 +1,44 @@
package net.schowek.nextclouddlna.dlna.upnp
import net.schowek.nextclouddlna.dlna.upnp.transport.ApacheStreamClient
import net.schowek.nextclouddlna.dlna.upnp.transport.ApacheStreamClientConfiguration
import net.schowek.nextclouddlna.dlna.upnp.transport.MyStreamServerConfiguration
import net.schowek.nextclouddlna.dlna.upnp.transport.MyStreamServerImpl
import net.schowek.nextclouddlna.util.ServerInfoProvider
import org.jupnp.DefaultUpnpServiceConfiguration
import org.jupnp.transport.impl.NetworkAddressFactoryImpl
import org.jupnp.transport.spi.DatagramIO
import org.jupnp.transport.spi.NetworkAddressFactory
import org.springframework.context.annotation.Profile
import org.springframework.stereotype.Component
import java.net.InetAddress
import java.net.NetworkInterface
@Component
@Profile("!integration")
class MyUpnpServiceConfiguration(
private val serverInfoProvider: ServerInfoProvider
) : DefaultUpnpServiceConfiguration(serverInfoProvider.port) {
val addressesToBind = listOf(serverInfoProvider.host)
override fun createStreamClient() =
ApacheStreamClient(ApacheStreamClientConfiguration(syncProtocolExecutorService))
override fun createStreamServer(networkAddressFactory: NetworkAddressFactory) =
MyStreamServerImpl(MyStreamServerConfiguration(networkAddressFactory.streamListenPort))
override fun createDatagramIO(networkAddressFactory: NetworkAddressFactory): DatagramIO<*> {
return super.createDatagramIO(networkAddressFactory)
}
override fun createNetworkAddressFactory(streamListenPort: Int, multicastResponsePort: Int) =
MyNetworkAddressFactory(streamListenPort, multicastResponsePort)
inner class MyNetworkAddressFactory(
streamListenPort: Int,
multicastResponsePort: Int
) : NetworkAddressFactoryImpl(streamListenPort, multicastResponsePort) {
override fun isUsableAddress(iface: NetworkInterface, address: InetAddress) =
addressesToBind.contains(address.hostAddress)
}
}

View file

@ -1,4 +1,4 @@
package net.schowek.nextclouddlna.dlna.transport
package net.schowek.nextclouddlna.dlna.upnp.transport
import mu.KLogging
import org.apache.http.HttpMessage

View file

@ -1,4 +1,4 @@
package net.schowek.nextclouddlna.dlna.transport
package net.schowek.nextclouddlna.dlna.upnp.transport
import org.jupnp.transport.spi.AbstractStreamClientConfiguration
import java.util.concurrent.ExecutorService

View file

@ -1,4 +1,4 @@
package net.schowek.nextclouddlna.dlna.transport
package net.schowek.nextclouddlna.dlna.upnp.transport
import org.jupnp.transport.spi.StreamServerConfiguration

View file

@ -1,4 +1,4 @@
package net.schowek.nextclouddlna.dlna.transport
package net.schowek.nextclouddlna.dlna.upnp.transport
import mu.KLogging
import org.jupnp.transport.Router

View file

@ -1,7 +1,7 @@
package net.schowek.nextclouddlna.nextcloud.content
import mu.KLogging
import net.schowek.nextclouddlna.nextcloud.NextcloudDB
import net.schowek.nextclouddlna.nextcloud.db.NextcloudDB
import org.springframework.scheduling.annotation.Scheduled
import org.springframework.stereotype.Component
import java.time.Clock
@ -100,8 +100,8 @@ class ContentTreeProvider(
}
}
fun getItem(id: String): ContentItem? = tree.getItem(id)
fun getNode(id: String): ContentNode? = tree.getNode(id)
fun getItem(id: String) = tree.getItem(id)
fun getNode(id: String) = tree.getNode(id)
companion object : KLogging() {
const val REBUILD_TREE_DELAY_IN_MS = 1000 * 60L // 1m
@ -115,13 +115,11 @@ class ContentTree {
private val nodes: MutableMap<String, ContentNode> = HashMap()
private val items: MutableMap<String, ContentItem> = HashMap()
fun getNode(id: String): ContentNode? {
return nodes[id]
}
val itemsCount get() = items.size
val nodesCount get() = nodes.size
fun getItem(id: String): ContentItem? {
return items[id]
}
fun getNode(id: String) = nodes[id]
fun getItem(id: String) = items[id]
fun addItem(item: ContentItem) {
items["${item.id}"] = item
@ -130,8 +128,5 @@ class ContentTree {
fun addNode(node: ContentNode) {
nodes["${node.id}"] = node
}
val itemsCount get() = items.size
val nodesCount get() = nodes.size
}

View file

@ -1,4 +1,4 @@
package net.schowek.nextclouddlna.nextcloud
package net.schowek.nextclouddlna.nextcloud.db
import jakarta.annotation.PostConstruct
import mu.KLogging
@ -6,7 +6,6 @@ import net.schowek.nextclouddlna.nextcloud.config.NextcloudConfigDiscovery
import net.schowek.nextclouddlna.nextcloud.content.ContentItem
import net.schowek.nextclouddlna.nextcloud.content.ContentNode
import net.schowek.nextclouddlna.nextcloud.content.MediaFormat
import net.schowek.nextclouddlna.nextcloud.db.*
import net.schowek.nextclouddlna.nextcloud.db.Filecache.Companion.FOLDER_MIME_TYPE
import org.springframework.dao.InvalidDataAccessResourceUsageException
import org.springframework.stereotype.Component

View file

@ -1,6 +1,6 @@
package net.schowek.nextclouddlna.nextcloud.content
import net.schowek.nextclouddlna.nextcloud.NextcloudDB
import net.schowek.nextclouddlna.nextcloud.db.NextcloudDB
import spock.lang.Specification
import java.time.Clock
import java.time.ZoneId