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:2.7.1'
implementation 'org.jupnp:org.jupnp.support: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' implementation 'org.apache.httpcomponents:httpclient:4.5.14'
// to avoid snakeyaml-1.3 vulnerability CVE-2022-1471 // to avoid snakeyaml-1.3 vulnerability CVE-2022-1471
implementation 'org.yaml:snakeyaml:2.2' implementation 'org.yaml:snakeyaml:2.2'

View file

@ -1,7 +1,7 @@
package net.schowek.nextclouddlna.controller package net.schowek.nextclouddlna.controller
import net.schowek.nextclouddlna.dlna.DlnaService
import net.schowek.nextclouddlna.dlna.media.MediaServer import net.schowek.nextclouddlna.dlna.MediaServer
import org.jupnp.support.model.DIDLObject import org.jupnp.support.model.DIDLObject
import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Autowired
import org.springframework.http.HttpStatus import org.springframework.http.HttpStatus
@ -14,11 +14,14 @@ class UpnpControllerIntTest extends UpnpAwareSpecification {
@Autowired @Autowired
private MediaServer mediaServer private MediaServer mediaServer
@Autowired
private DlnaService dlnaService
def uid def uid
def setup() { def setup() {
uid = mediaServer.serviceIdentifier uid = mediaServer.serviceIdentifier
dlnaService.start()
} }
def "should serve icon"() { 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.SpringBootContextLoader
import org.springframework.boot.test.context.SpringBootTest import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.web.client.TestRestTemplate 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.ActiveProfiles
import org.springframework.test.context.ContextConfiguration import org.springframework.test.context.ContextConfiguration
import spock.lang.Specification import spock.lang.Specification
import support.beans.TestConfig
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.DEFINED_PORT 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) @ContextConfiguration(loader = SpringBootContextLoader, classes = NextcloudDLNAApp.class)
@SpringBootTest(webEnvironment = DEFINED_PORT) @SpringBootTest(webEnvironment = DEFINED_PORT)
@ActiveProfiles("integration") @ActiveProfiles("integration")
@Import(TestConfig.class)
@DirtiesContext(classMode = AFTER_CLASS)
class IntegrationSpecification extends Specification { class IntegrationSpecification extends Specification {
@Autowired @Autowired
private TestRestTemplate restTemplate private TestRestTemplate restTemplate
@ -27,6 +33,6 @@ class IntegrationSpecification extends Specification {
private ServerInfoProvider serverInfoProvider private ServerInfoProvider serverInfoProvider
protected String urlWithPort(String uri = "") { 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.context.annotation.Profile
import org.springframework.stereotype.Component 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.beans.factory.annotation.Autowired
import org.springframework.context.annotation.Profile import org.springframework.context.annotation.Profile
import org.springframework.stereotype.Component 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.beans.factory.annotation.Value
import org.springframework.boot.web.server.ConfigurableWebServerFactory 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 jakarta.servlet.http.HttpServletRequest
import mu.KLogging 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.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.InputStreamResource
import org.springframework.core.io.Resource import org.springframework.core.io.Resource
import org.springframework.http.MediaType.APPLICATION_OCTET_STREAM_VALUE 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 mu.KLogging
import net.schowek.nextclouddlna.util.ExternalUrls 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 mu.KLogging
import org.apache.http.HttpMessage 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 org.jupnp.transport.spi.AbstractStreamClientConfiguration
import java.util.concurrent.ExecutorService 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 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 mu.KLogging
import org.jupnp.transport.Router import org.jupnp.transport.Router

View file

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

View file

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