Skip to content

Commit

Permalink
Merge pull request #487 from kagemomiji/issue474-fix-upnp-not-playable
Browse files Browse the repository at this point in the history
#474 fix auth bug at stream endpoint from upnp player
  • Loading branch information
kagemomiji committed May 31, 2024
2 parents 8e3fb0d + e03837a commit 885a11f
Show file tree
Hide file tree
Showing 21 changed files with 660 additions and 540 deletions.
4 changes: 3 additions & 1 deletion airsonic-main/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,12 @@
<dependency>
<groupId>io.dropwizard.metrics</groupId>
<artifactId>metrics-core</artifactId>
<version>4.2.25</version>
</dependency>
<dependency>
<groupId>io.dropwizard.metrics</groupId>
<artifactId>metrics-jmx</artifactId>
<version>4.2.25</version>
</dependency>
<!-- END Metrics -->

Expand Down Expand Up @@ -464,7 +466,7 @@
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-artifact</artifactId>
<version>3.9.6</version>
<version>3.9.7</version>
</dependency>

<dependency>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,12 +114,13 @@ public ResponseEntity<Resource> handleRequest(Authentication authentication,
@RequestParam(required = false, name = "offsetSeconds") Double offsetSeconds,
ServletWebRequest swr) throws Exception {

User user = securityService.getCurrentUser(swr.getRequest());
String username = securityService.getCurrentUsername(swr.getRequest());
User user = securityService.getUserByName(username);
if (!(authentication instanceof JWTAuthenticationToken) && !user.isStreamRole()) {
throw new AccessDeniedException("Streaming is forbidden for user " + user.getUsername());
throw new AccessDeniedException("Streaming is forbidden for user " + username);
}

Player player = playerService.getPlayer(swr.getRequest(), swr.getResponse(), user.getUsername(), false, true);
Player player = playerService.getPlayer(swr.getRequest(), swr.getResponse(), username, false, true);

Long expectedSize = null;

Expand Down Expand Up @@ -154,8 +155,8 @@ public ResponseEntity<Resource> handleRequest(Authentication authentication,
if (isSingleFile) {

if (!(authentication instanceof JWTAuthenticationToken)
&& !securityService.isFolderAccessAllowed(file, user.getUsername())) {
throw new AccessDeniedException("Access to file " + file.getId() + " is forbidden for user " + user.getUsername());
&& !securityService.isFolderAccessAllowed(file, username)) {
throw new AccessDeniedException("Access to file " + file.getId() + " is forbidden for user " + username);
}

// Update the index of the currently playing media file. At
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import jakarta.annotation.Nullable;
import jakarta.servlet.http.HttpServletRequest;

import java.io.IOException;
Expand Down Expand Up @@ -307,7 +308,6 @@ public List<UserCredential> getCredentials(String username, App... apps) {
return userRepository.findByUsername(username).map(user -> {
return userCredentialRepository.findByUserAndAppIn(user, List.of(apps));
}).orElseGet(() -> {
LOG.warn("Can't get credentials for a non-existent user {}", username);
return Collections.emptyList();
});
}
Expand Down Expand Up @@ -420,7 +420,8 @@ public String getCurrentUsername(HttpServletRequest request) {
* @param username The username used when logging in.
* @return The user, or <code>null</code> if not found.
*/
public User getUserByName(String username) {
@Nullable
public User getUserByName(@Nullable String username) {
return getUserByName(username, true);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,15 @@
You should have received a copy of the GNU General Public License
along with Airsonic. If not, see <http://www.gnu.org/licenses/>.
Copyright 2024 (C) Y.Tory
Copyright 2016 (C) Airsonic Authors
Based upon Subsonic, Copyright 2009 (C) Sindre Mehus
*/
package org.airsonic.player.service;

import org.airsonic.player.service.upnp.ApacheUpnpServiceConfiguration;
import org.airsonic.player.service.upnp.CustomContentDirectory;
import org.airsonic.player.service.upnp.MSMediaReceiverRegistrarService;
import org.airsonic.player.util.FileUtil;
import org.fourthline.cling.DefaultUpnpServiceConfiguration;
import org.fourthline.cling.UpnpService;
import org.fourthline.cling.UpnpServiceImpl;
import org.fourthline.cling.binding.annotations.AnnotationLocalServiceBinder;
Expand All @@ -36,13 +36,6 @@
import org.fourthline.cling.support.model.ProtocolInfos;
import org.fourthline.cling.support.model.dlna.DLNAProfiles;
import org.fourthline.cling.support.model.dlna.DLNAProtocolInfo;
import org.fourthline.cling.transport.impl.apache.StreamClientConfigurationImpl;
import org.fourthline.cling.transport.impl.apache.StreamClientImpl;
import org.fourthline.cling.transport.impl.apache.StreamServerConfigurationImpl;
import org.fourthline.cling.transport.impl.apache.StreamServerImpl;
import org.fourthline.cling.transport.spi.NetworkAddressFactory;
import org.fourthline.cling.transport.spi.StreamClient;
import org.fourthline.cling.transport.spi.StreamServer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
Expand All @@ -51,7 +44,9 @@

import jakarta.annotation.PostConstruct;

import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
Expand All @@ -67,9 +62,14 @@ public class UPnPService {

private static final Logger LOG = LoggerFactory.getLogger(UPnPService.class);

private final static int MIN_ADVERTISEMENT_AGE_SECONDS = 60 * 60 * 24;

@Autowired
private SettingsService settingsService;

@Autowired
private VersionService versionService;

private UpnpService upnpService;

@Autowired
Expand Down Expand Up @@ -103,20 +103,15 @@ public void ensureServiceStarted() {

public void ensureServiceStopped() {
running.getAndUpdate(bo -> {
if (bo) {
if (upnpService != null) {
LOG.info("Disabling UPnP/DLNA media server");
upnpService.getRegistry().removeAllLocalDevices();
System.err.println("Shutting down UPnP service...");
upnpService.shutdown();
System.err.println("Shutting down UPnP service - Done!");
}
return false;
} else {
return false;
if (upnpService != null && bo) {
LOG.info("Disabling UPnP/DLNA media server");
upnpService.getRegistry().removeAllLocalDevices();
LOG.info("Shutting down UPnP service...");
upnpService.shutdown();
LOG.info("Shutting down UPnP service - Done!");
}
return false;
});

}

private void startService() {
Expand All @@ -132,7 +127,8 @@ private void startService() {
private synchronized void createService() {
upnpService = new UpnpServiceImpl(new ApacheUpnpServiceConfiguration(settingsService.getUPnpPort()));

// Asynch search for other devices (most importantly UPnP-enabled routers for port-mapping)
// Asynch search for other devices (most importantly UPnP-enabled routers for
// port-mapping)
upnpService.getControlPoint().search();

}
Expand All @@ -153,26 +149,9 @@ public void setMediaServerEnabled(boolean enabled) {

private LocalDevice createMediaServerDevice() throws Exception {

String serverName = settingsService.getDlnaServerName();
String serverId = settingsService.getDlnaServerId();
if (serverId == null) {
serverId = UUID.randomUUID().toString();
settingsService.setDlnaServerId(serverId);
}
DeviceIdentity identity = new DeviceIdentity(UDN.valueOf(serverId));
DeviceType type = new UDADeviceType("MediaServer", 1);

// TODO: DLNACaps

DeviceDetails details = new DeviceDetails(serverName, new ManufacturerDetails(serverName),
new ModelDetails(serverName),
new DLNADoc[]{new DLNADoc("DMS", DLNADoc.Version.V1_5)}, null);

InputStream in = getClass().getResourceAsStream("logo-512.png");
Icon icon = new Icon("image/png", 512, 512, 32, "logo-512", in);
FileUtil.closeQuietly(in);

LocalService<CustomContentDirectory> contentDirectoryservice = new AnnotationLocalServiceBinder().read(CustomContentDirectory.class);
@SuppressWarnings("unchecked")
LocalService<CustomContentDirectory> contentDirectoryservice = new AnnotationLocalServiceBinder()
.read(CustomContentDirectory.class);
contentDirectoryservice.setManager(new DefaultServiceManager<CustomContentDirectory>(contentDirectoryservice) {

@Override
Expand All @@ -193,25 +172,61 @@ protected CustomContentDirectory createServiceInstance() {
}
}

LocalService<ConnectionManagerService> connetionManagerService = new AnnotationLocalServiceBinder().read(ConnectionManagerService.class);
connetionManagerService.setManager(new DefaultServiceManager<ConnectionManagerService>(connetionManagerService) {
@Override
protected ConnectionManagerService createServiceInstance() {
return new ConnectionManagerService(protocols, null);
}
});
@SuppressWarnings("unchecked")
LocalService<ConnectionManagerService> connetionManagerService = new AnnotationLocalServiceBinder()
.read(ConnectionManagerService.class);
connetionManagerService
.setManager(new DefaultServiceManager<ConnectionManagerService>(connetionManagerService) {
@Override
protected ConnectionManagerService createServiceInstance() {
return new ConnectionManagerService(protocols, null);
}
});

// For compatibility with Microsoft
LocalService<MSMediaReceiverRegistrarService> receiverService = new AnnotationLocalServiceBinder().read(MSMediaReceiverRegistrarService.class);
@SuppressWarnings("unchecked")
LocalService<MSMediaReceiverRegistrarService> receiverService = new AnnotationLocalServiceBinder()
.read(MSMediaReceiverRegistrarService.class);
receiverService.setManager(new DefaultServiceManager<>(receiverService, MSMediaReceiverRegistrarService.class));

return new LocalDevice(identity, type, details, new Icon[]{icon}, new LocalService[]{contentDirectoryservice, connetionManagerService, receiverService});
Icon icon = null;
try (InputStream in = getClass().getResourceAsStream("logo-512.png")) {
icon = new Icon("image/png", 512, 512, 32, "logo-512", in);
} catch (IOException e) {
throw new RuntimeException(e);
}

String serverName = settingsService.getDlnaServerName();
String serverId = settingsService.getDlnaServerId();
String serialNumber = versionService.getLocalBuildNumber();
if (serverId == null) {
serverId = UUID.randomUUID().toString();
settingsService.setDlnaServerId(serverId);
}

// TODO: DLNACaps
DLNADoc[] dlnaDocs = new DLNADoc[] { new DLNADoc("DMS", DLNADoc.Version.V1_5) };
URI modelURI = URI.create("https://airsonic.github.io/");
URI manufacturerURI = URI.create("https://github.com/kagemomiji/airsonic-advanced");
URI presentaionURI = URI.create(settingsService.getDlnaBaseLANURL());
ManufacturerDetails manufacturerDetails = new ManufacturerDetails(serverName, modelURI);
ModelDetails modelDetails = new ModelDetails(serverName, null, versionService.getLocalVersion().toString(),
manufacturerURI);
DeviceDetails details = new DeviceDetails(serverName, manufacturerDetails, modelDetails, serialNumber, null,
presentaionURI, dlnaDocs, null);
DeviceIdentity identity = new DeviceIdentity(UDN.uniqueSystemIdentifier(serverName),
MIN_ADVERTISEMENT_AGE_SECONDS);
DeviceType type = new UDADeviceType("MediaServer", 1);

return new LocalDevice(identity, type, details, new Icon[] { icon },
new LocalService[] { contentDirectoryservice, connetionManagerService, receiverService });
}

public List<String> getSonosControllerHosts() {
ensureServiceStarted();
List<String> result = new ArrayList<String>();
for (Device device : upnpService.getRegistry().getDevices(new DeviceType("schemas-upnp-org", "ZonePlayer"))) {
for (Device<?, ?, ?> device : upnpService.getRegistry()
.getDevices(new DeviceType("schemas-upnp-org", "ZonePlayer"))) {
if (device instanceof RemoteDevice) {
URL descriptorURL = ((RemoteDevice) device).getIdentity().getDescriptorURL();
if (descriptorURL != null) {
Expand All @@ -221,36 +236,4 @@ public List<String> getSonosControllerHosts() {
}
return result;
}

public UpnpService getUpnpService() {
return upnpService;
}

public void setSettingsService(SettingsService settingsService) {
this.settingsService = settingsService;
}

public void setCustomContentDirectory(CustomContentDirectory customContentDirectory) {
this.dispatchingContentDirectory = customContentDirectory;
}

/**
* Note the different packages on similarly named classes from the parent
*
*/
public static class ApacheUpnpServiceConfiguration extends DefaultUpnpServiceConfiguration {
public ApacheUpnpServiceConfiguration(int streamListenPort) {
super(streamListenPort);
}

@Override
public StreamClient<?> createStreamClient() {
return new StreamClientImpl(new StreamClientConfigurationImpl(getSyncProtocolExecutorService()));
}

@Override
public StreamServer<?> createStreamServer(NetworkAddressFactory networkAddressFactory) {
return new StreamServerImpl(new StreamServerConfigurationImpl(networkAddressFactory.getStreamListenPort()));
}
}
}
Loading

0 comments on commit 885a11f

Please sign in to comment.