Lock Down unsecured urls

Signed-off-by: Andrew DeMaria <lostonamountain@gmail.com>
master
Andrew DeMaria 8 years ago
parent f13ed83d77
commit f7bd43136b
No known key found for this signature in database
GPG Key ID: 0A3F5E91F8364EDF
  1. 7
      libresonic-main/pom.xml
  2. 16
      libresonic-main/src/main/java/org/libresonic/player/controller/ControllerUtils.java
  3. 2
      libresonic-main/src/main/java/org/libresonic/player/controller/CoverArtController.java
  4. 79
      libresonic-main/src/main/java/org/libresonic/player/controller/ExternalPlayerController.java
  5. 30
      libresonic-main/src/main/java/org/libresonic/player/controller/HLSController.java
  6. 13
      libresonic-main/src/main/java/org/libresonic/player/controller/ShareSettingsController.java
  7. 10
      libresonic-main/src/main/java/org/libresonic/player/controller/StreamController.java
  8. 324
      libresonic-main/src/main/java/org/libresonic/player/domain/MediaFileWithUrlInfo.java
  9. 169
      libresonic-main/src/main/java/org/libresonic/player/security/GlobalSecurityConfig.java
  10. 100
      libresonic-main/src/main/java/org/libresonic/player/security/JWTAuthenticationProvider.java
  11. 31
      libresonic-main/src/main/java/org/libresonic/player/security/JWTAuthenticationToken.java
  12. 123
      libresonic-main/src/main/java/org/libresonic/player/security/JWTRequestParameterProcessingFilter.java
  13. 112
      libresonic-main/src/main/java/org/libresonic/player/security/WebSecurityConfig.java
  14. 80
      libresonic-main/src/main/java/org/libresonic/player/service/JWTSecurityService.java
  15. 12
      libresonic-main/src/main/java/org/libresonic/player/service/ServiceLocator.java
  16. 10
      libresonic-main/src/main/java/org/libresonic/player/service/SettingsService.java
  17. 28
      libresonic-main/src/main/java/org/libresonic/player/service/ShareService.java
  18. 4
      libresonic-main/src/main/java/org/libresonic/player/service/VersionService.java
  19. 8
      libresonic-main/src/main/java/org/libresonic/player/service/upnp/FolderBasedContentDirectory.java
  20. 19
      libresonic-main/src/main/java/org/libresonic/player/service/upnp/LibresonicContentDirectory.java
  21. 9
      libresonic-main/src/main/resources/applicationContext-service.xml
  22. 21
      libresonic-main/src/main/webapp/WEB-INF/jsp/externalPlayer.jsp
  23. 2
      libresonic-main/src/main/webapp/WEB-INF/jsp/shareSettings.jsp
  24. 62
      libresonic-main/src/test/java/org/libresonic/player/service/JWTSecurityServiceTest.java

@ -68,6 +68,11 @@
<groupId>org.springframework.security</groupId> <groupId>org.springframework.security</groupId>
<artifactId>spring-security-ldap</artifactId> <artifactId>spring-security-ldap</artifactId>
</dependency> </dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.1.0</version>
</dependency>
<dependency> <dependency>
<groupId>org.springframework.ldap</groupId> <groupId>org.springframework.ldap</groupId>
<artifactId>spring-ldap-core</artifactId> <artifactId>spring-ldap-core</artifactId>
@ -115,7 +120,7 @@
<dependency> <dependency>
<groupId>commons-codec</groupId> <groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId> <artifactId>commons-codec</artifactId>
<version>1.2</version> <version>1.10</version>
</dependency> </dependency>
<dependency> <dependency>

@ -1,9 +1,25 @@
package org.libresonic.player.controller; package org.libresonic.player.controller;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.servlet.HandlerMapping;
import javax.servlet.http.HttpServletRequest;
/** /**
* This class has been created to refactor code previously present * This class has been created to refactor code previously present
* in the MultiController. * in the MultiController.
*/ */
public class ControllerUtils { public class ControllerUtils {
public static String extractMatched(final HttpServletRequest request){
String path = (String) request.getAttribute(
HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE);
String bestMatchPattern = (String ) request.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE);
AntPathMatcher apm = new AntPathMatcher();
return apm.extractPathWithinPattern(bestMatchPattern, path);
}
} }

@ -57,7 +57,7 @@ import java.util.concurrent.Semaphore;
* @author Sindre Mehus * @author Sindre Mehus
*/ */
@Controller @Controller
@RequestMapping("/coverArt") @RequestMapping(value = {"/coverArt", "/ext/coverArt"})
public class CoverArtController implements LastModified { public class CoverArtController implements LastModified {
public static final String ALBUM_COVERART_PREFIX = "al-"; public static final String ALBUM_COVERART_PREFIX = "al-";

@ -19,30 +19,28 @@
*/ */
package org.libresonic.player.controller; package org.libresonic.player.controller;
import com.auth0.jwt.interfaces.DecodedJWT;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.libresonic.player.Logger; import org.libresonic.player.domain.*;
import org.libresonic.player.domain.MediaFile; import org.libresonic.player.security.JWTAuthenticationToken;
import org.libresonic.player.domain.MusicFolder; import org.libresonic.player.service.*;
import org.libresonic.player.domain.Player; import org.slf4j.Logger;
import org.libresonic.player.domain.Share; import org.slf4j.LoggerFactory;
import org.libresonic.player.service.MediaFileService;
import org.libresonic.player.service.PlayerService;
import org.libresonic.player.service.SettingsService;
import org.libresonic.player.service.ShareService;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.util.UrlPathHelper; import org.springframework.web.util.UriComponentsBuilder;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import java.io.IOException; import java.io.IOException;
import java.util.*; import java.util.*;
import java.util.stream.Collectors;
import static org.libresonic.player.controller.ExternalPlayerController.SHARE_PATH;
/** /**
* Controller for the page used to play shared music (Twitter, Facebook etc). * Controller for the page used to play shared music (Twitter, Facebook etc).
@ -50,12 +48,10 @@ import static org.libresonic.player.controller.ExternalPlayerController.SHARE_PA
* @author Sindre Mehus * @author Sindre Mehus
*/ */
@Controller @Controller
@RequestMapping(SHARE_PATH + "**") @RequestMapping(value = {"/ext/share/**"})
public class ExternalPlayerController { public class ExternalPlayerController {
private static final Logger LOG = Logger.getLogger(ExternalPlayerController.class); private static final Logger LOG = LoggerFactory.getLogger(ExternalPlayerController.class);
public static final String SHARE_PATH = "/share/";
@Autowired @Autowired
private SettingsService settingsService; private SettingsService settingsService;
@ -65,15 +61,16 @@ public class ExternalPlayerController {
private ShareService shareService; private ShareService shareService;
@Autowired @Autowired
private MediaFileService mediaFileService; private MediaFileService mediaFileService;
@Autowired
private JWTSecurityService jwtSecurityService;
@RequestMapping(method = RequestMethod.GET) @RequestMapping(method = RequestMethod.GET)
protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception { protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception {
Map<String, Object> map = new HashMap<>(); Map<String, Object> map = new HashMap<>();
String pathRelativeToBase = new UrlPathHelper().getPathWithinApplication(request); String shareName = ControllerUtils.extractMatched(request);
LOG.debug("Share name is {}", shareName);
String shareName = StringUtils.removeStart(pathRelativeToBase, SHARE_PATH);
if(StringUtils.isBlank(shareName)) { if(StringUtils.isBlank(shareName)) {
LOG.warn("Could not find share with shareName " + shareName); LOG.warn("Could not find share with shareName " + shareName);
@ -97,24 +94,32 @@ public class ExternalPlayerController {
Player player = playerService.getGuestPlayer(request); Player player = playerService.getGuestPlayer(request);
map.put("share", share); map.put("share", share);
map.put("songs", getSongs(share, player.getUsername())); map.put("songs", getSongs(request, share, player));
map.put("player", player.getId());
return new ModelAndView("externalPlayer", "model", map); return new ModelAndView("externalPlayer", "model", map);
} }
private List<MediaFile> getSongs(Share share, String username) throws IOException { private List<MediaFileWithUrlInfo> getSongs(HttpServletRequest request, Share share, Player player) throws IOException {
List<MediaFile> result = new ArrayList<MediaFile>(); Date expires = null;
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if(authentication instanceof JWTAuthenticationToken) {
DecodedJWT token = jwtSecurityService.verify((String) authentication.getCredentials());
expires = token.getExpiresAt();
}
Date finalExpires = expires;
List<MediaFileWithUrlInfo> result = new ArrayList<>();
List<MusicFolder> musicFolders = settingsService.getMusicFoldersForUser(username); List<MusicFolder> musicFolders = settingsService.getMusicFoldersForUser(player.getUsername());
if (share != null) { if (share != null) {
for (MediaFile file : shareService.getSharedFiles(share.getId(), musicFolders)) { for (MediaFile file : shareService.getSharedFiles(share.getId(), musicFolders)) {
if (file.getFile().exists()) { if (file.getFile().exists()) {
if (file.isDirectory()) { if (file.isDirectory()) {
result.addAll(mediaFileService.getChildrenOf(file, true, false, true)); List<MediaFile> childrenOf = mediaFileService.getChildrenOf(file, true, false, true);
result.addAll(childrenOf.stream().map(mf -> addUrlInfo(request, player, mf, finalExpires)).collect(Collectors.toList()));
} else { } else {
result.add(file); result.add(addUrlInfo(request, player, file, finalExpires));
} }
} }
} }
@ -122,4 +127,26 @@ public class ExternalPlayerController {
return result; return result;
} }
public MediaFileWithUrlInfo addUrlInfo(HttpServletRequest request, Player player, MediaFile mediaFile, Date expires) {
String prefix = "/ext";
String streamUrl = jwtSecurityService.addJWTToken(
UriComponentsBuilder
.fromHttpUrl(NetworkService.getBaseUrl(request) + prefix + "/stream")
.queryParam("id", mediaFile.getId())
.queryParam("player", player.getId())
.queryParam("maxBitRate", "1200"),
expires)
.build()
.toUriString();
String coverArtUrl = jwtSecurityService.addJWTToken(
UriComponentsBuilder
.fromHttpUrl(NetworkService.getBaseUrl(request) + prefix + "/coverArt.view")
.queryParam("id", mediaFile.getId())
.queryParam("size", "500"),
expires)
.build()
.toUriString();
return new MediaFileWithUrlInfo(mediaFile, coverArtUrl, streamUrl);
}
} }

@ -22,6 +22,7 @@ package org.libresonic.player.controller;
import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.StringUtils;
import org.libresonic.player.domain.MediaFile; import org.libresonic.player.domain.MediaFile;
import org.libresonic.player.domain.Player; import org.libresonic.player.domain.Player;
import org.libresonic.player.service.JWTSecurityService;
import org.libresonic.player.service.MediaFileService; import org.libresonic.player.service.MediaFileService;
import org.libresonic.player.service.PlayerService; import org.libresonic.player.service.PlayerService;
import org.libresonic.player.service.SecurityService; import org.libresonic.player.service.SecurityService;
@ -33,6 +34,7 @@ import org.springframework.web.bind.ServletRequestUtils;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.util.UriComponentsBuilder;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
@ -50,7 +52,7 @@ import java.util.regex.Pattern;
* @author Sindre Mehus * @author Sindre Mehus
*/ */
@Controller(value = "hlsController") @Controller(value = "hlsController")
@RequestMapping("/hls/**") @RequestMapping(value = {"/hls/**", "/ext/hls/**"})
public class HLSController { public class HLSController {
private static final int SEGMENT_DURATION = 10; private static final int SEGMENT_DURATION = 10;
@ -62,6 +64,8 @@ public class HLSController {
private MediaFileService mediaFileService; private MediaFileService mediaFileService;
@Autowired @Autowired
private SecurityService securityService; private SecurityService securityService;
@Autowired
private JWTSecurityService jwtSecurityService;
@RequestMapping(method = RequestMethod.GET) @RequestMapping(method = RequestMethod.GET)
public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception { public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
@ -142,7 +146,12 @@ public class HLSController {
for (Pair<Integer, Dimension> bitRate : bitRates) { for (Pair<Integer, Dimension> bitRate : bitRates) {
Integer kbps = bitRate.getFirst(); Integer kbps = bitRate.getFirst();
writer.println("#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=" + kbps * 1000L); writer.println("#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=" + kbps * 1000L);
writer.print(contextPath + "hls/hls.m3u8?id=" + id + "&player=" + player.getId() + "&bitRate=" + kbps); UriComponentsBuilder url = (UriComponentsBuilder.fromUriString(contextPath + "ext/hls/hls.m3u8")
.queryParam("id", id)
.queryParam("player", player.getId())
.queryParam("bitRate", kbps));
jwtSecurityService.addJWTToken(url);
writer.print(url.toUriString());
Dimension dimension = bitRate.getSecond(); Dimension dimension = bitRate.getSecond();
if (dimension != null) { if (dimension != null) {
writer.print("@" + dimension.width + "x" + dimension.height); writer.print("@" + dimension.width + "x" + dimension.height);
@ -173,17 +182,22 @@ public class HLSController {
} }
private String createStreamUrl(HttpServletRequest request, Player player, int id, int offset, int duration, Pair<Integer, Dimension> bitRate) { private String createStreamUrl(HttpServletRequest request, Player player, int id, int offset, int duration, Pair<Integer, Dimension> bitRate) {
StringBuilder builder = new StringBuilder(); UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(getContextPath(request) + "ext/stream/stream.ts");
builder.append(getContextPath(request)).append("stream/stream.ts?id=").append(id).append("&hls=true&timeOffset=").append(offset) builder.queryParam("id", id);
.append("&player=").append(player.getId()).append("&duration=").append(duration); builder.queryParam("hls", "true");
builder.queryParam("timeOffset", offset);
builder.queryParam("player", player.getId());
builder.queryParam("duration", duration);
if (bitRate != null) { if (bitRate != null) {
builder.append("&maxBitRate=").append(bitRate.getFirst()); builder.queryParam("maxBitRate", bitRate.getFirst());
Dimension dimension = bitRate.getSecond(); Dimension dimension = bitRate.getSecond();
if (dimension != null) { if (dimension != null) {
builder.append("&size=").append(dimension.width).append("x").append(dimension.height); builder.queryParam("size", dimension.width);
builder.queryParam("x", dimension.height);
} }
} }
return builder.toString(); jwtSecurityService.addJWTToken(builder);
return builder.toUriString();
} }
private String getContextPath(HttpServletRequest request) { private String getContextPath(HttpServletRequest request) {

@ -63,7 +63,6 @@ public class ShareSettingsController {
public String doGet(HttpServletRequest request, Model model) throws Exception { public String doGet(HttpServletRequest request, Model model) throws Exception {
Map<String, Object> map = new HashMap<String, Object>(); Map<String, Object> map = new HashMap<String, Object>();
map.put("shareBaseUrl", shareService.getShareBaseUrl(request));
map.put("shareInfos", getShareInfos(request)); map.put("shareInfos", getShareInfos(request));
map.put("user", securityService.getCurrentUser(request)); map.put("user", securityService.getCurrentUser(request));
@ -121,7 +120,9 @@ public class ShareSettingsController {
List<MediaFile> files = shareService.getSharedFiles(share.getId(), musicFolders); List<MediaFile> files = shareService.getSharedFiles(share.getId(), musicFolders);
if (!files.isEmpty()) { if (!files.isEmpty()) {
MediaFile file = files.get(0); MediaFile file = files.get(0);
result.add(new ShareInfo(share, file.isDirectory() ? file : mediaFileService.getParentOf(file))); result.add(new ShareInfo(shareService.getShareUrl(request, share), share, file.isDirectory() ? file :
mediaFileService
.getParentOf(file)));
} }
} }
return result; return result;
@ -144,10 +145,12 @@ public class ShareSettingsController {
} }
public static class ShareInfo { public static class ShareInfo {
private final String shareUrl;
private final Share share; private final Share share;
private final MediaFile dir; private final MediaFile dir;
public ShareInfo(Share share, MediaFile dir) { public ShareInfo(String shareUrl, Share share, MediaFile dir) {
this.shareUrl = shareUrl;
this.share = share; this.share = share;
this.dir = dir; this.dir = dir;
} }
@ -156,6 +159,10 @@ public class ShareSettingsController {
return share; return share;
} }
public String getShareUrl() {
return shareUrl;
}
public MediaFile getDir() { public MediaFile getDir() {
return dir; return dir;
} }

@ -25,12 +25,15 @@ import org.libresonic.player.domain.*;
import org.libresonic.player.io.PlayQueueInputStream; import org.libresonic.player.io.PlayQueueInputStream;
import org.libresonic.player.io.RangeOutputStream; import org.libresonic.player.io.RangeOutputStream;
import org.libresonic.player.io.ShoutCastOutputStream; import org.libresonic.player.io.ShoutCastOutputStream;
import org.libresonic.player.security.JWTAuthenticationToken;
import org.libresonic.player.service.*; import org.libresonic.player.service.*;
import org.libresonic.player.service.sonos.SonosHelper; import org.libresonic.player.service.sonos.SonosHelper;
import org.libresonic.player.util.HttpRange; import org.libresonic.player.util.HttpRange;
import org.libresonic.player.util.StringUtil; import org.libresonic.player.util.StringUtil;
import org.libresonic.player.util.Util; import org.libresonic.player.util.Util;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.web.bind.ServletRequestBindingException; import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.bind.ServletRequestUtils; import org.springframework.web.bind.ServletRequestUtils;
@ -55,7 +58,7 @@ import java.util.regex.Pattern;
* @author Sindre Mehus * @author Sindre Mehus
*/ */
@Controller @Controller
@RequestMapping("/stream/**") @RequestMapping(value = {"/stream/**", "/ext/stream/**"})
public class StreamController { public class StreamController {
private static final Logger LOG = Logger.getLogger(StreamController.class); private static final Logger LOG = Logger.getLogger(StreamController.class);
@ -86,10 +89,11 @@ public class StreamController {
PlayQueueInputStream in = null; PlayQueueInputStream in = null;
Player player = playerService.getPlayer(request, response, false, true); Player player = playerService.getPlayer(request, response, false, true);
User user = securityService.getUserByName(player.getUsername()); User user = securityService.getUserByName(player.getUsername());
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
try { try {
if (!user.isStreamRole()) { if (!(authentication instanceof JWTAuthenticationToken) && !user.isStreamRole()) {
response.sendError(HttpServletResponse.SC_FORBIDDEN, "Streaming is forbidden for user " + user.getUsername()); response.sendError(HttpServletResponse.SC_FORBIDDEN, "Streaming is forbidden for user " + user.getUsername());
return null; return null;
} }
@ -128,7 +132,7 @@ public class StreamController {
if (isSingleFile) { if (isSingleFile) {
if (!securityService.isFolderAccessAllowed(file, user.getUsername())) { if (!(authentication instanceof JWTAuthenticationToken) && !securityService.isFolderAccessAllowed(file, user.getUsername())) {
response.sendError(HttpServletResponse.SC_FORBIDDEN, response.sendError(HttpServletResponse.SC_FORBIDDEN,
"Access to file " + file.getId() + " is forbidden for user " + user.getUsername()); "Access to file " + file.getId() + " is forbidden for user " + user.getUsername());
return null; return null;

@ -0,0 +1,324 @@
package org.libresonic.player.domain;
import com.google.common.base.Function;
import java.io.File;
import java.util.Date;
import java.util.List;
public class MediaFileWithUrlInfo {
private final MediaFile file;
private final String coverArtUrl;
private final String streamUrl;
public MediaFileWithUrlInfo(MediaFile file, String coverArtUrl, String streamUrl) {
this.file = file;
this.coverArtUrl = coverArtUrl;
this.streamUrl = streamUrl;
}
public String getStreamUrl() {
return streamUrl;
}
public String getCoverArtUrl() {
return coverArtUrl;
}
public int getId() {
return file.getId();
}
public void setId(int id) {
file.setId(id);
}
public String getPath() {
return file.getPath();
}
public void setPath(String path) {
file.setPath(path);
}
public String getFolder() {
return file.getFolder();
}
public void setFolder(String folder) {
file.setFolder(folder);
}
public File getFile() {
return file.getFile();
}
public boolean exists() {
return file.exists();
}
public MediaFile.MediaType getMediaType() {
return file.getMediaType();
}
public void setMediaType(MediaFile.MediaType mediaType) {
file.setMediaType(mediaType);
}
public boolean isVideo() {
return file.isVideo();
}
public boolean isAudio() {
return file.isAudio();
}
public String getFormat() {
return file.getFormat();
}
public void setFormat(String format) {
file.setFormat(format);
}
public boolean isDirectory() {
return file.isDirectory();
}
public boolean isFile() {
return file.isFile();
}
public boolean isAlbum() {
return file.isAlbum();
}
public String getTitle() {
return file.getTitle();
}
public void setTitle(String title) {
file.setTitle(title);
}
public String getAlbumName() {
return file.getAlbumName();
}
public void setAlbumName(String album) {
file.setAlbumName(album);
}
public String getArtist() {
return file.getArtist();
}
public void setArtist(String artist) {
file.setArtist(artist);
}
public String getAlbumArtist() {
return file.getAlbumArtist();
}
public void setAlbumArtist(String albumArtist) {
file.setAlbumArtist(albumArtist);
}
public String getName() {
return file.getName();
}
public Integer getDiscNumber() {
return file.getDiscNumber();
}
public void setDiscNumber(Integer discNumber) {
file.setDiscNumber(discNumber);
}
public Integer getTrackNumber() {
return file.getTrackNumber();
}
public void setTrackNumber(Integer trackNumber) {
file.setTrackNumber(trackNumber);
}
public Integer getYear() {
return file.getYear();
}
public void setYear(Integer year) {
file.setYear(year);
}
public String getGenre() {
return file.getGenre();
}
public void setGenre(String genre) {
file.setGenre(genre);
}
public Integer getBitRate() {
return file.getBitRate();
}
public void setBitRate(Integer bitRate) {
file.setBitRate(bitRate);
}
public boolean isVariableBitRate() {
return file.isVariableBitRate();
}
public void setVariableBitRate(boolean variableBitRate) {
file.setVariableBitRate(variableBitRate);
}
public Integer getDurationSeconds() {
return file.getDurationSeconds();
}
public void setDurationSeconds(Integer durationSeconds) {
file.setDurationSeconds(durationSeconds);
}
public String getDurationString() {
return file.getDurationString();
}
public Long getFileSize() {
return file.getFileSize();
}
public void setFileSize(Long fileSize) {
file.setFileSize(fileSize);
}
public Integer getWidth() {
return file.getWidth();
}
public void setWidth(Integer width) {
file.setWidth(width);
}
public Integer getHeight() {
return file.getHeight();
}
public void setHeight(Integer height) {
file.setHeight(height);
}
public String getCoverArtPath() {
return file.getCoverArtPath();
}
public void setCoverArtPath(String coverArtPath) {
file.setCoverArtPath(coverArtPath);
}
public String getParentPath() {
return file.getParentPath();
}
public void setParentPath(String parentPath) {
file.setParentPath(parentPath);
}
public File getParentFile() {
return file.getParentFile();
}
public int getPlayCount() {
return file.getPlayCount();
}
public void setPlayCount(int playCount) {
file.setPlayCount(playCount);
}
public Date getLastPlayed() {
return file.getLastPlayed();
}
public void setLastPlayed(Date lastPlayed) {
file.setLastPlayed(lastPlayed);
}
public String getComment() {
return file.getComment();
}
public void setComment(String comment) {
file.setComment(comment);
}
public Date getCreated() {
return file.getCreated();
}
public void setCreated(Date created) {
file.setCreated(created);
}
public Date getChanged() {
return file.getChanged();
}
public void setChanged(Date changed) {
file.setChanged(changed);
}
public Date getLastScanned() {
return file.getLastScanned();
}
public void setLastScanned(Date lastScanned) {
file.setLastScanned(lastScanned);
}
public Date getStarredDate() {
return file.getStarredDate();
}
public void setStarredDate(Date starredDate) {
file.setStarredDate(starredDate);
}
public Date getChildrenLastUpdated() {
return file.getChildrenLastUpdated();
}
public void setChildrenLastUpdated(Date childrenLastUpdated) {
file.setChildrenLastUpdated(childrenLastUpdated);
}
public boolean isPresent() {
return file.isPresent();
}
public void setPresent(boolean present) {
file.setPresent(present);
}
public int getVersion() {
return file.getVersion();
}
public File getCoverArtFile() {
return file.getCoverArtFile();
}
public static List<Integer> toIdList(List<MediaFile> from) {
return MediaFile.toIdList(from);
}
public static Function<MediaFile, Integer> toId() {
return MediaFile.toId();
}
}

@ -0,0 +1,169 @@
package org.libresonic.player.security;
import org.apache.commons.lang3.StringUtils;
import org.libresonic.player.service.JWTSecurityService;
import org.libresonic.player.service.SecurityService;
import org.libresonic.player.service.SettingsService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.security.SecurityProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.authentication.configurers.GlobalAuthenticationConfigurerAdapter;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
@Configuration
@Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class GlobalSecurityConfig extends GlobalAuthenticationConfigurerAdapter {
private static Logger logger = LoggerFactory.getLogger(GlobalSecurityConfig.class);
static final String FAILURE_URL = "/login?error=1";
@Autowired
private SecurityService securityService;
@Autowired
private CsrfSecurityRequestMatcher csrfSecurityRequestMatcher;
@Autowired
LoginFailureLogger loginFailureLogger;
@Autowired
SettingsService settingsService;
@Autowired
LibresonicUserDetailsContextMapper libresonicUserDetailsContextMapper;
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
if (settingsService.isLdapEnabled()) {
auth.ldapAuthentication()
.contextSource()
.managerDn(settingsService.getLdapManagerDn())
.managerPassword(settingsService.getLdapManagerPassword())
.url(settingsService.getLdapUrl())
.and()
.userSearchFilter(settingsService.getLdapSearchFilter())
.userDetailsContextMapper(libresonicUserDetailsContextMapper);
}
auth.userDetailsService(securityService);
String jwtKey = settingsService.getJWTKey();
if(StringUtils.isBlank(jwtKey)) {
logger.warn("Generating new jwt key");
jwtKey = JWTSecurityService.generateKey();
settingsService.setJWTKey(jwtKey);
settingsService.save();
}
auth.authenticationProvider(new JWTAuthenticationProvider(jwtKey));
}
@Configuration
@Order(1)
public class ExtSecurityConfiguration extends WebSecurityConfigurerAdapter {
public ExtSecurityConfiguration() {
super(true);
}
@Bean(name = "jwtAuthenticationFilter")
public JWTRequestParameterProcessingFilter jwtAuthFilter() throws Exception {
return new JWTRequestParameterProcessingFilter(authenticationManager(), FAILURE_URL);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http = http.addFilter(new WebAsyncManagerIntegrationFilter());
http = http.addFilterBefore(jwtAuthFilter(), UsernamePasswordAuthenticationFilter.class);
http
.antMatcher("/ext/**")
.csrf().requireCsrfProtectionMatcher(csrfSecurityRequestMatcher).and()
.headers().frameOptions().sameOrigin().and()
.authorizeRequests()
.antMatchers("/ext/stream/**", "/ext/coverArt.view", "/ext/share/**", "/ext/hls/**")
.hasAnyRole("TEMP", "USER").and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.exceptionHandling().and()
.securityContext().and()
.requestCache().and()
.anonymous().and()
.servletApi();
}
}
@Configuration
@Order(2)
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
RESTRequestParameterProcessingFilter restAuthenticationFilter = new RESTRequestParameterProcessingFilter();
restAuthenticationFilter.setAuthenticationManager(authenticationManagerBean());
restAuthenticationFilter.setSecurityService(securityService);
restAuthenticationFilter.setLoginFailureLogger(loginFailureLogger);
http = http.addFilterBefore(restAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
http
.csrf()
.requireCsrfProtectionMatcher(csrfSecurityRequestMatcher)
.and().headers()
.frameOptions()
.sameOrigin()
.and().authorizeRequests()
.antMatchers("recover.view", "accessDenied.view",
"/style/**", "/icons/**", "/flash/**", "/script/**",
"/sonos/**", "/crossdomain.xml", "/login", "/error")
.permitAll()
.antMatchers("/personalSettings.view", "/passwordSettings.view",
"/playerSettings.view", "/shareSettings.view", "/passwordSettings.view")
.hasRole("SETTINGS")
.antMatchers("/generalSettings.view", "/advancedSettings.view", "/userSettings.view",
"/musicFolderSettings.view", "/databaseSettings.view")
.hasRole("ADMIN")
.antMatchers("/deletePlaylist.view", "/savePlaylist.view")
.hasRole("PLAYLIST")
.antMatchers("/download.view")
.hasRole("DOWNLOAD")
.antMatchers("/upload.view")
.hasRole("UPLOAD")
.antMatchers("/createShare.view")
.hasRole("SHARE")
.antMatchers("/changeCoverArt.view", "/editTags.view")
.hasRole("COVERART")
.antMatchers("/setMusicFileInfo.view")
.hasRole("COMMENT")
.antMatchers("/podcastReceiverAdmin.view")
.hasRole("PODCAST")
.antMatchers("/**")
.hasRole("USER")
.anyRequest().authenticated()
.and().formLogin()
.loginPage("/login")
.permitAll()
.defaultSuccessUrl("/index.view", true)
.failureUrl(FAILURE_URL)
.usernameParameter("j_username")
.passwordParameter("j_password")
// see http://docs.spring.io/spring-security/site/docs/3.2.4.RELEASE/reference/htmlsingle/#csrf-logout
.and().logout().logoutRequestMatcher(new AntPathRequestMatcher("/logout", "GET")).logoutSuccessUrl(
"/login?logout")
.and().rememberMe().key("libresonic");
}
}
}

@ -0,0 +1,100 @@
package org.libresonic.player.security;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.google.common.collect.MapDifference;
import com.google.common.collect.Maps;
import org.apache.commons.lang3.StringUtils;
import org.libresonic.player.service.JWTSecurityService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
public class JWTAuthenticationProvider implements AuthenticationProvider {
private static final Logger logger = LoggerFactory.getLogger(JWTAuthenticationProvider.class);
private final String jwtKey;
public JWTAuthenticationProvider(String jwtSignAndVerifyKey) {
this.jwtKey = jwtSignAndVerifyKey;
}
@Override
public Authentication authenticate(Authentication auth) throws AuthenticationException {
JWTAuthenticationToken authentication = (JWTAuthenticationToken) auth;
if(authentication.getCredentials() == null || !(authentication.getCredentials() instanceof String)) {
logger.error("Credentials not present");
return null;
}
String rawToken = (String) auth.getCredentials();
DecodedJWT token = JWTSecurityService.verify(jwtKey, rawToken);
Claim path = token.getClaim(JWTSecurityService.CLAIM_PATH);
authentication.setAuthenticated(true);
// TODO:AD This is super unfortunate, but not sure there is a better way when using JSP
if(StringUtils.contains(authentication.getRequestedPath(), "/WEB-INF/jsp/")) {
logger.warn("BYPASSING AUTH FOR WEB-INF page");
} else
if(!roughlyEqual(path.asString(), authentication.getRequestedPath())) {
throw new InsufficientAuthenticationException("Credentials not valid for path " + authentication
.getRequestedPath() + ". They are valid for " + path.asString());
}
List<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority("IS_AUTHENTICATED_FULLY"));
authorities.add(new SimpleGrantedAuthority("ROLE_TEMP"));
return new JWTAuthenticationToken(authorities, rawToken, authentication.getRequestedPath());
}
private static boolean roughlyEqual(String expectedRaw, String requestedPathRaw) {
logger.debug("Comparing expected [{}] vs requested [{}]", expectedRaw, requestedPathRaw);
if(StringUtils.isEmpty(expectedRaw)) {
logger.debug("False: empty expected");
return false;
}
try {
UriComponents expected = UriComponentsBuilder.fromUriString(expectedRaw).build();
UriComponents requested = UriComponentsBuilder.fromUriString(requestedPathRaw).build();
if(!Objects.equals(expected.getPath(), requested.getPath())) {
logger.debug("False: expected path [{}] does not match requested path [{}]",
expected.getPath(), requested.getPath());
return false;
}
MapDifference<String, List<String>> difference = Maps.difference(expected.getQueryParams(),
requested.getQueryParams());
if(difference.entriesDiffering().size() != 0 ||
difference.entriesOnlyOnLeft().size() != 0 ||
difference.entriesOnlyOnRight().size() != 1 ||
difference.entriesOnlyOnRight().get(JWTSecurityService.JWT_PARAM_NAME) == null) {
logger.debug("False: expected query params [{}] do not match requested query params [{}]", expected.getQueryParams(), requested.getQueryParams());
return false;
}
} catch(Exception e) {
logger.warn("Exception encountered while comparing paths", e);
return false;
}
return true;
}
@Override
public boolean supports(Class<?> authentication) {
return JWTAuthenticationToken.class.isAssignableFrom(authentication);
}
}

@ -0,0 +1,31 @@
package org.libresonic.player.security;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import java.util.Collection;
public class JWTAuthenticationToken extends AbstractAuthenticationToken {
private final String token;
private String requestedPath;
public JWTAuthenticationToken(Collection<? extends GrantedAuthority> authorities, String token, String requestedPath) {
super(authorities);
this.token = token;
this.requestedPath = requestedPath;
}
@Override
public Object getCredentials() {
return token;
}
@Override
public Object getPrincipal() {
return "GERNIC_JWT_PRINICPLE";
}
public String getRequestedPath() {
return requestedPath;
}
}

@ -0,0 +1,123 @@
package org.libresonic.player.security;
import org.libresonic.player.service.JWTSecurityService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.InternalAuthenticationServiceException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.util.StringUtils;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Optional;
public class JWTRequestParameterProcessingFilter implements Filter {
private static final Logger logger = LoggerFactory.getLogger(JWTRequestParameterProcessingFilter.class);
private final AuthenticationManager authenticationManager;
private final AuthenticationFailureHandler failureHandler;
protected JWTRequestParameterProcessingFilter(AuthenticationManager authenticationManager, String failureUrl) {
this.authenticationManager = authenticationManager; failureHandler = new SimpleUrlAuthenticationFailureHandler(failureUrl);
}
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
Optional<JWTAuthenticationToken> token = findToken(request);
if(token.isPresent()) {
return authenticationManager.authenticate(token.get());
}
throw new AuthenticationServiceException("Invalid auth method");
}
private static Optional<JWTAuthenticationToken> findToken(HttpServletRequest request) {
String token = request.getParameter(JWTSecurityService.JWT_PARAM_NAME);
if(!StringUtils.isEmpty(token)) {
return Optional.of(new JWTAuthenticationToken(AuthorityUtils.NO_AUTHORITIES, token, request.getRequestURI() + "?" + request.getQueryString()));
}
return Optional.empty();
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException,
ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) resp;
if(!findToken(request).isPresent()) {
chain.doFilter(req, resp);
return;
}
if (logger.isDebugEnabled()) {
logger.debug("Request is to process authentication");
}
Authentication authResult;
try {
authResult = attemptAuthentication(request, response);
if (authResult == null) {
// return immediately as subclass has indicated that it hasn't completed
// authentication
return;
}
}
catch (InternalAuthenticationServiceException failed) {
logger.error(
"An internal error occurred while trying to authenticate the user.",
failed);
unsuccessfulAuthentication(request, response, failed);
return;
}
catch (AuthenticationException failed) {
// Authentication failed
unsuccessfulAuthentication(request, response, failed);
return;
}
if (logger.isDebugEnabled()) {
logger.debug("Authentication success. Updating SecurityContextHolder to contain: "
+ authResult);
}
SecurityContextHolder.getContext().setAuthentication(authResult);
chain.doFilter(request, response);
}
protected void unsuccessfulAuthentication(HttpServletRequest request,
HttpServletResponse response, AuthenticationException failed)
throws IOException, ServletException {
SecurityContextHolder.clearContext();
if (logger.isDebugEnabled()) {
logger.debug("Authentication request failed: " + failed.toString(), failed);
logger.debug("Updated SecurityContextHolder to contain null Authentication");
logger.debug("Delegating to authentication failure handler " + failureHandler);
}
failureHandler.onAuthenticationFailure(request, response, failed);
}
@Override
public void destroy() {
}
}

@ -1,112 +0,0 @@
package org.libresonic.player.security;
import org.libresonic.player.service.SecurityService;
import org.libresonic.player.service.SettingsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private SecurityService securityService;
@Autowired
private CsrfSecurityRequestMatcher csrfSecurityRequestMatcher;
@Autowired
LoginFailureLogger loginFailureLogger;
@Autowired
SettingsService settingsService;
@Autowired
LibresonicUserDetailsContextMapper libresonicUserDetailsContextMapper;
@Override
@Bean(name = "authenticationManager")
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
if (settingsService.isLdapEnabled()) {
auth.ldapAuthentication()
.contextSource()
.managerDn(settingsService.getLdapManagerDn())
.managerPassword(settingsService.getLdapManagerPassword())
.url(settingsService.getLdapUrl())
.and()
.userSearchFilter(settingsService.getLdapSearchFilter())
.userDetailsContextMapper(libresonicUserDetailsContextMapper);
}
auth.userDetailsService(securityService);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
RESTRequestParameterProcessingFilter restAuthenticationFilter = new RESTRequestParameterProcessingFilter();
restAuthenticationFilter.setAuthenticationManager((AuthenticationManager) getApplicationContext().getBean("authenticationManager"));
restAuthenticationFilter.setSecurityService(securityService);
restAuthenticationFilter.setLoginFailureLogger(loginFailureLogger);
http = http.addFilterBefore(restAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
http
.csrf()
.requireCsrfProtectionMatcher(csrfSecurityRequestMatcher)
.and().headers()
.frameOptions()
.sameOrigin()
.and().authorizeRequests()
.antMatchers("recover.view", "accessDenied.view",
"coverArt.view", "/hls/**", "/stream/**", "/ws/**",
"/share/**", "/style/**", "/icons/**",
"/flash/**", "/script/**", "/sonos/**", "/crossdomain.xml", "/login")
.permitAll()
.antMatchers("/personalSettings.view", "/passwordSettings.view",
"/playerSettings.view", "/shareSettings.view","/passwordSettings.view")
.hasRole("SETTINGS")
.antMatchers("/generalSettings.view","/advancedSettings.view","/userSettings.view",
"/musicFolderSettings.view", "/databaseSettings.view")
.hasRole("ADMIN")
.antMatchers("/deletePlaylist.view","/savePlaylist.view")
.hasRole("PLAYLIST")
.antMatchers("/download.view")
.hasRole("DOWNLOAD")
.antMatchers("/upload.view")
.hasRole("UPLOAD")
.antMatchers("/createShare.view")
.hasRole("SHARE")
.antMatchers("/changeCoverArt.view","/editTags.view")
.hasRole("COVERART")
.antMatchers("/setMusicFileInfo.view")
.hasRole("COMMENT")
.antMatchers("/podcastReceiverAdmin.view")
.hasRole("PODCAST")
.antMatchers("/**")
.hasRole("USER")
.anyRequest().authenticated()
.and().formLogin()
.loginPage("/login")
.permitAll()
.defaultSuccessUrl("/index.view", true)
.failureUrl("/login?error=1")
.usernameParameter("j_username")
.passwordParameter("j_password")
// see http://docs.spring.io/spring-security/site/docs/3.2.4.RELEASE/reference/htmlsingle/#csrf-logout
.and().logout().logoutRequestMatcher(new AntPathRequestMatcher("/logout", "GET")).logoutSuccessUrl("/login?logout")
.and().rememberMe().key("libresonic");
}
}

@ -0,0 +1,80 @@
package org.libresonic.player.service;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.DateUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.security.SecureRandom;
import java.util.Date;
public class JWTSecurityService {
private static final Logger logger = LoggerFactory.getLogger(JWTSecurityService.class);
public static final String JWT_PARAM_NAME = "jwt";
public static final String CLAIM_PATH = "path";
// TODO make this configurable
public static final int DEFAULT_DAYS_VALID_FOR = 7;
private static SecureRandom secureRandom = new SecureRandom();
private final SettingsService settingsService;
public JWTSecurityService(SettingsService settingsService) {
this.settingsService = settingsService;
}
public static String generateKey() {
BigInteger randomInt = new BigInteger(130, secureRandom);
return randomInt.toString(32);
}
public static Algorithm getAlgorithm(String jwtKey) {
try {
return Algorithm.HMAC256(jwtKey);
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
private static String createToken(String jwtKey, String path, Date expireDate) {
UriComponents components = UriComponentsBuilder.fromUriString(path).build();
String query = components.getQuery();
String claim = components.getPath() + (!StringUtils.isBlank(query) ? "?" + components.getQuery() : "");
logger.debug("Creating token with claim " + claim);
return JWT.create()
.withClaim(CLAIM_PATH, claim)
.withExpiresAt(expireDate)
.sign(getAlgorithm(jwtKey));
}
public UriComponentsBuilder addJWTToken(UriComponentsBuilder builder) {
return addJWTToken(builder, DateUtils.addDays(new Date(), DEFAULT_DAYS_VALID_FOR));
}
public UriComponentsBuilder addJWTToken(UriComponentsBuilder builder, Date expires) {
String token = JWTSecurityService.createToken(
settingsService.getJWTKey(),
builder.toUriString(),
expires);
builder.queryParam(JWTSecurityService.JWT_PARAM_NAME, token);
return builder;
}
public static DecodedJWT verify(String jwtKey, String token) {
Algorithm algorithm = JWTSecurityService.getAlgorithm(jwtKey);
JWTVerifier verifier = JWT.require(algorithm).build();
return verifier.verify(token);
}
public DecodedJWT verify(String credentials) {
return verify(settingsService.getJWTKey(), credentials);
}
}

@ -28,10 +28,6 @@ package org.libresonic.player.service;
public class ServiceLocator { public class ServiceLocator {
private static SettingsService settingsService; private static SettingsService settingsService;
private static VersionService versionService;
private ServiceLocator() {
}
public static SettingsService getSettingsService() { public static SettingsService getSettingsService() {
return settingsService; return settingsService;
@ -40,13 +36,5 @@ public class ServiceLocator {
public static void setSettingsService(SettingsService settingsService) { public static void setSettingsService(SettingsService settingsService) {
ServiceLocator.settingsService = settingsService; ServiceLocator.settingsService = settingsService;
} }
public static VersionService getVersionService() {
return versionService;
}
public static void setVersionService(VersionService versionService) {
ServiceLocator.versionService = versionService;
}
} }

@ -98,6 +98,7 @@ public class SettingsService {
private static final String KEY_SONOS_ENABLED = "SonosEnabled"; private static final String KEY_SONOS_ENABLED = "SonosEnabled";
private static final String KEY_SONOS_SERVICE_NAME = "SonosServiceName"; private static final String KEY_SONOS_SERVICE_NAME = "SonosServiceName";
private static final String KEY_SONOS_SERVICE_ID = "SonosServiceId"; private static final String KEY_SONOS_SERVICE_ID = "SonosServiceId";
private static final String KEY_JWT_KEY = "JWTKey";
private static final String KEY_SMTP_SERVER = "SmtpServer"; private static final String KEY_SMTP_SERVER = "SmtpServer";
private static final String KEY_SMTP_ENCRYPTION = "SmtpEncryption"; private static final String KEY_SMTP_ENCRYPTION = "SmtpEncryption";
@ -117,6 +118,7 @@ public class SettingsService {
private static final String KEY_DATABASE_USERTABLE_QUOTE = "DatabaseUsertableQuote"; private static final String KEY_DATABASE_USERTABLE_QUOTE = "DatabaseUsertableQuote";
// Default values. // Default values.
private static final String DEFAULT_JWT_KEY = null;
private static final String DEFAULT_INDEX_STRING = "A B C D E F G H I J K L M N O P Q R S T U V W X-Z(XYZ)"; private static final String DEFAULT_INDEX_STRING = "A B C D E F G H I J K L M N O P Q R S T U V W X-Z(XYZ)";
private static final String DEFAULT_IGNORED_ARTICLES = "The El La Los Las Le Les"; private static final String DEFAULT_IGNORED_ARTICLES = "The El La Los Las Le Les";
private static final String DEFAULT_SHORTCUTS = "New Incoming Podcast"; private static final String DEFAULT_SHORTCUTS = "New Incoming Podcast";
@ -1338,6 +1340,14 @@ public class SettingsService {
setString(KEY_DATABASE_USERTABLE_QUOTE, usertableQuote); setString(KEY_DATABASE_USERTABLE_QUOTE, usertableQuote);
} }
public String getJWTKey() {
return getString(KEY_JWT_KEY, DEFAULT_JWT_KEY);
}
public void setJWTKey(String jwtKey) {
setString(KEY_JWT_KEY, jwtKey);
}
public void setConfigurationService(ApacheCommonsConfigurationService configurationService) { public void setConfigurationService(ApacheCommonsConfigurationService configurationService) {
this.configurationService = configurationService; this.configurationService = configurationService;
} }

@ -23,17 +23,12 @@ import org.apache.commons.lang.ObjectUtils;
import org.apache.commons.lang.RandomStringUtils; import org.apache.commons.lang.RandomStringUtils;
import org.libresonic.player.Logger; import org.libresonic.player.Logger;
import org.libresonic.player.dao.ShareDao; import org.libresonic.player.dao.ShareDao;
import org.libresonic.player.domain.MediaFile; import org.libresonic.player.domain.*;
import org.libresonic.player.domain.MusicFolder; import org.springframework.web.util.UriComponentsBuilder;
import org.libresonic.player.domain.Share;
import org.libresonic.player.domain.User;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import java.util.ArrayList; import java.util.*;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
/** /**
* Provides services for sharing media. * Provides services for sharing media.
@ -47,8 +42,8 @@ public class ShareService {
private ShareDao shareDao; private ShareDao shareDao;
private SecurityService securityService; private SecurityService securityService;
private SettingsService settingsService;
private MediaFileService mediaFileService; private MediaFileService mediaFileService;
private JWTSecurityService jwtSecurityService;
public List<Share> getAllShares() { public List<Share> getAllShares() {
return shareDao.getAllShares(); return shareDao.getAllShares();
@ -115,12 +110,9 @@ public class ShareService {
shareDao.deleteShare(id); shareDao.deleteShare(id);
} }
public String getShareBaseUrl(HttpServletRequest request) {
return NetworkService.getBaseUrl(request) + "/share/";
}
public String getShareUrl(HttpServletRequest request, Share share) { public String getShareUrl(HttpServletRequest request, Share share) {
return getShareBaseUrl(request) + share.getName(); String shareUrl = NetworkService.getBaseUrl(request) + "/ext/share/" + share.getName();
return jwtSecurityService.addJWTToken(UriComponentsBuilder.fromUriString(shareUrl), share.getExpires()).build().toUriString();
} }
public void setSecurityService(SecurityService securityService) { public void setSecurityService(SecurityService securityService) {
@ -131,11 +123,11 @@ public class ShareService {
this.shareDao = shareDao; this.shareDao = shareDao;
} }
public void setSettingsService(SettingsService settingsService) {
this.settingsService = settingsService;
}
public void setMediaFileService(MediaFileService mediaFileService) { public void setMediaFileService(MediaFileService mediaFileService) {
this.mediaFileService = mediaFileService; this.mediaFileService = mediaFileService;
} }
public void setJwtSecurityService(JWTSecurityService jwtSecurityService) {
this.jwtSecurityService = jwtSecurityService;
}
} }

@ -68,10 +68,6 @@ public class VersionService {
*/ */
private static final String VERSION_URL = "http://libresonic.org/release/version.txt"; private static final String VERSION_URL = "http://libresonic.org/release/version.txt";
public void init() {
ServiceLocator.setVersionService(this);
}
/** /**
* Returns the version number for the locally installed Libresonic version. * Returns the version number for the locally installed Libresonic version.
* *

@ -33,6 +33,7 @@ import org.libresonic.player.domain.*;
import org.libresonic.player.service.MediaFileService; import org.libresonic.player.service.MediaFileService;
import org.libresonic.player.service.PlaylistService; import org.libresonic.player.service.PlaylistService;
import org.libresonic.player.util.Util; import org.libresonic.player.util.Util;
import org.springframework.web.util.UriComponentsBuilder;
import java.net.URI; import java.net.URI;
import java.net.URISyntaxException; import java.net.URISyntaxException;
@ -260,7 +261,12 @@ public class FolderBasedContentDirectory extends LibresonicContentDirectory {
} }
private URI getAlbumArtUrl(MediaFile album) throws URISyntaxException { private URI getAlbumArtUrl(MediaFile album) throws URISyntaxException {
return new URI(getBaseUrl() + "coverArt.view?id=" + album.getId() + "&size=" + CoverArtScheme.LARGE.getSize()); return jwtSecurityService.addJWTToken(UriComponentsBuilder.fromUriString(getBaseUrl() + "/ext/coverArt.view")
.queryParam("id", album.getId())
.queryParam("size", CoverArtScheme.LARGE.getSize()))
.build()
.encode()
.toUri();
} }
public void setMediaFileService(MediaFileService mediaFileService) { public void setMediaFileService(MediaFileService mediaFileService) {

@ -30,11 +30,13 @@ import org.fourthline.cling.support.model.Res;
import org.fourthline.cling.support.model.SortCriterion; import org.fourthline.cling.support.model.SortCriterion;
import org.libresonic.player.domain.MediaFile; import org.libresonic.player.domain.MediaFile;
import org.libresonic.player.domain.Player; import org.libresonic.player.domain.Player;
import org.libresonic.player.service.JWTSecurityService;
import org.libresonic.player.service.PlayerService; import org.libresonic.player.service.PlayerService;
import org.libresonic.player.service.SettingsService; import org.libresonic.player.service.SettingsService;
import org.libresonic.player.service.TranscodingService; import org.libresonic.player.service.TranscodingService;
import org.libresonic.player.util.StringUtil; import org.libresonic.player.util.StringUtil;
import org.seamless.util.MimeType; import org.seamless.util.MimeType;
import org.springframework.web.util.UriComponentsBuilder;
/** /**
* @author Sindre Mehus * @author Sindre Mehus
@ -47,14 +49,23 @@ public abstract class LibresonicContentDirectory extends AbstractContentDirector
protected SettingsService settingsService; protected SettingsService settingsService;
private PlayerService playerService; private PlayerService playerService;
private TranscodingService transcodingService; private TranscodingService transcodingService;
protected JWTSecurityService jwtSecurityService;
protected Res createResourceForSong(MediaFile song) { protected Res createResourceForSong(MediaFile song) {
Player player = playerService.getGuestPlayer(null); Player player = playerService.getGuestPlayer(null);
String url = getBaseUrl() + "stream?id=" + song.getId() + "&player=" + player.getId();
UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(getBaseUrl() + "/ext/stream")
.queryParam("id", song.getId())
.queryParam("player", player.getId());
if (song.isVideo()) { if (song.isVideo()) {
url += "&format=" + TranscodingService.FORMAT_RAW; builder.queryParam("format", TranscodingService.FORMAT_RAW);
} }
jwtSecurityService.addJWTToken(builder);
String url = builder.toUriString();
String suffix = song.isVideo() ? FilenameUtils.getExtension(song.getPath()) : transcodingService.getSuffix(player, song, null); String suffix = song.isVideo() ? FilenameUtils.getExtension(song.getPath()) : transcodingService.getSuffix(player, song, null);
String mimeTypeString = StringUtil.getMimeType(suffix); String mimeTypeString = StringUtil.getMimeType(suffix);
MimeType mimeType = mimeTypeString == null ? null : MimeType.valueOf(mimeTypeString); MimeType mimeType = mimeTypeString == null ? null : MimeType.valueOf(mimeTypeString);
@ -123,4 +134,8 @@ public abstract class LibresonicContentDirectory extends AbstractContentDirector
public void setSettingsService(SettingsService settingsService) { public void setSettingsService(SettingsService settingsService) {
this.settingsService = settingsService; this.settingsService = settingsService;
} }
public void setJwtSecurityService(JWTSecurityService jwtSecurityService) {
this.jwtSecurityService = jwtSecurityService;
}
} }

@ -137,7 +137,7 @@
<property name="playlistDao" ref="playlistDao"/> <property name="playlistDao" ref="playlistDao"/>
</bean> </bean>
<bean id="versionService" class="org.libresonic.player.service.VersionService" init-method="init"/> <bean id="versionService" class="org.libresonic.player.service.VersionService" />
<bean id="statusService" class="org.libresonic.player.service.StatusService"> <bean id="statusService" class="org.libresonic.player.service.StatusService">
<property name="mediaFileService" ref="mediaFileService"/> <property name="mediaFileService" ref="mediaFileService"/>
@ -166,9 +166,9 @@
<bean id="shareService" class="org.libresonic.player.service.ShareService"> <bean id="shareService" class="org.libresonic.player.service.ShareService">
<property name="shareDao" ref="shareDao"/> <property name="shareDao" ref="shareDao"/>
<property name="settingsService" ref="settingsService"/>
<property name="securityService" ref="securityService"/> <property name="securityService" ref="securityService"/>
<property name="mediaFileService" ref="mediaFileService"/> <property name="mediaFileService" ref="mediaFileService"/>
<property name="jwtSecurityService" ref="jwtSecurityService" />
</bean> </bean>
<bean id="podcastService" class="org.libresonic.player.service.PodcastService" init-method="init"> <bean id="podcastService" class="org.libresonic.player.service.PodcastService" init-method="init">
@ -198,6 +198,7 @@
<property name="playerService" ref="playerService"/> <property name="playerService" ref="playerService"/>
<property name="transcodingService" ref="transcodingService"/> <property name="transcodingService" ref="transcodingService"/>
<property name="mediaFileService" ref="mediaFileService"/> <property name="mediaFileService" ref="mediaFileService"/>
<property name="jwtSecurityService" ref="jwtSecurityService" />
</bean> </bean>
<bean id="upnpService" class="org.libresonic.player.service.UPnPService" init-method="init"> <bean id="upnpService" class="org.libresonic.player.service.UPnPService" init-method="init">
@ -299,4 +300,8 @@
<property name="settingsService" ref="settingsService"/> <property name="settingsService" ref="settingsService"/>
</bean> </bean>
<bean id="jwtSecurityService" class="org.libresonic.player.service.JWTSecurityService">
<constructor-arg ref="settingsService" />
</bean>
</beans> </beans>

@ -10,12 +10,8 @@
<meta name="og:type" content="album"/> <meta name="og:type" content="album"/>
<c:if test="${not empty model.songs}"> <c:if test="${not empty model.songs}">
<sub:url value="/coverArt.view" var="coverArtUrl">
<sub:param name="id" value="${model.songs[0].id}"/>
<sub:param name="size" value="500"/>
</sub:url>
<meta name="og:title" content="${fn:escapeXml(model.songs[0].artist)} &mdash; ${fn:escapeXml(model.songs[0].albumName)}"/> <meta name="og:title" content="${fn:escapeXml(model.songs[0].artist)} &mdash; ${fn:escapeXml(model.songs[0].albumName)}"/>
<meta name="og:image" content="${coverArtUrl}"/> <meta name="og:image" content="${model.songs[0].coverArtUrl}"/>
</c:if> </c:if>
<script type="text/javascript"> <script type="text/javascript">
@ -45,21 +41,12 @@
var list = new Array(); var list = new Array();
<c:forEach items="${model.songs}" var="song" varStatus="loopStatus"> <c:forEach items="${model.songs}" var="song" varStatus="loopStatus">
<%--@elvariable id="song" type="org.libresonic.player.domain.MediaFile"--%> <%--@elvariable id="song" type="org.libresonic.player.domain.MediaFileWithUrlInfo"--%>
<sub:url value="/stream" var="streamUrl">
<sub:param name="id" value="${song.id}"/>
<sub:param name="player" value="${model.player}"/>
<sub:param name="maxBitRate" value="1200"/>
</sub:url>
<sub:url value="/coverArt.view" var="coverUrl">
<sub:param name="id" value="${song.id}"/>
<sub:param name="size" value="500"/>
</sub:url>
// TODO: Use video provider for aac, m4a // TODO: Use video provider for aac, m4a
list[${loopStatus.count - 1}] = { list[${loopStatus.count - 1}] = {
file: "${streamUrl}", file: "${song.streamUrl}",
image: "${coverUrl}", image: "${song.coverArtUrl}",
title: "${fn:escapeXml(song.title)}", title: "${fn:escapeXml(song.title)}",
provider: "${song.video ? "video" : "sound"}", provider: "${song.video ? "video" : "sound"}",
description: "${fn:escapeXml(song.artist)}" description: "${fn:escapeXml(song.artist)}"

@ -37,7 +37,7 @@
</c:url> </c:url>
<tr> <tr>
<td style="padding-left:1em"><a href="${model.shareBaseUrl}${share.name}" target="_blank">${share.name}</a></td> <td style="padding-left:1em"><a href="${shareInfo.shareUrl}" target="_blank">${share.name}</a></td>
<td style="padding-left:1em">${fn:escapeXml(share.username)}</td> <td style="padding-left:1em">${fn:escapeXml(share.username)}</td>
<td style="padding-left:1em"><input type="text" name="description[${share.id}]" size="40" value="${share.description}"/></td> <td style="padding-left:1em"><input type="text" name="description[${share.id}]" size="40" value="${share.description}"/></td>
<td style="padding-left:1em"><fmt:formatDate value="${share.lastVisited}" type="date" dateStyle="medium"/></td> <td style="padding-left:1em"><fmt:formatDate value="${share.lastVisited}" type="date" dateStyle="medium"/></td>

@ -0,0 +1,62 @@
package org.libresonic.player.service;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.springframework.web.util.UriComponentsBuilder;
import java.util.Arrays;
import java.util.Collection;
import static org.junit.Assert.assertEquals;
@RunWith(Parameterized.class)
public class JWTSecurityServiceTest {
private final String key = "someKey";
private final JWTSecurityService service = new JWTSecurityService(settingsWithKey(key));
private final String uriString;
private final Algorithm algorithm = JWTSecurityService.getAlgorithm(key);
private final JWTVerifier verifier = JWT.require(algorithm).build();
private final String expectedClaimString;
@Parameterized.Parameters
public static Collection<Object[]> data() {
return Arrays.asList(new Object[][] {
{ "http://localhost:8080/libresonic/stream?id=4", "/libresonic/stream?id=4" },
{ "/libresonic/stream?id=4", "/libresonic/stream?id=4" },
});
}
public JWTSecurityServiceTest(String uriString, String expectedClaimString) {
this.uriString = uriString;
this.expectedClaimString = expectedClaimString;
}
@Test
public void addJWTToken() throws Exception {
UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(uriString);
String actualUri = service.addJWTToken(builder).build().toUriString();
String jwtToken = UriComponentsBuilder.fromUriString(actualUri).build().getQueryParams().getFirst(
JWTSecurityService.JWT_PARAM_NAME);
DecodedJWT verify = verifier.verify(jwtToken);
Claim claim = verify.getClaim(JWTSecurityService.CLAIM_PATH);
assertEquals(expectedClaimString, claim.asString());
}
private SettingsService settingsWithKey(String jwtKey) {
return new SettingsService() {
@Override
public String getJWTKey() {
return jwtKey;
}
};
}
}
Loading…
Cancel
Save