Lock Down unsecured urls
Signed-off-by: Andrew DeMaria <lostonamountain@gmail.com>
This commit is contained in:
@@ -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);
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -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-";
|
||||||
|
|||||||
+53
-26
@@ -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<MusicFolder> musicFolders = settingsService.getMusicFoldersForUser(username);
|
List<MediaFileWithUrlInfo> result = new ArrayList<>();
|
||||||
|
|
||||||
|
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) {
|
||||||
|
|||||||
+10
-3
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-3
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
+169
@@ -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");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
+100
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+31
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+123
@@ -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.
|
||||||
*
|
*
|
||||||
|
|||||||
+7
-1
@@ -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) {
|
||||||
|
|||||||
+17
-2
@@ -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)} — ${fn:escapeXml(model.songs[0].albumName)}"/>
|
<meta name="og:title" content="${fn:escapeXml(model.songs[0].artist)} — ${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>
|
||||||
|
|||||||
+62
@@ -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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user