ported legacy functions
This commit is contained in:
@@ -1,5 +1,20 @@
|
||||
/* © SRSoftware 2025 */
|
||||
package de.srsoftware.umbrella.legacy;
|
||||
|
||||
import static de.srsoftware.tools.MimeType.MIME_FORM_URL;
|
||||
import static de.srsoftware.tools.Query.decode;
|
||||
import static de.srsoftware.umbrella.core.Constants.*;
|
||||
import static de.srsoftware.umbrella.core.Constants.TOKEN;
|
||||
import static de.srsoftware.umbrella.core.Paths.*;
|
||||
import static de.srsoftware.umbrella.core.Util.request;
|
||||
import static de.srsoftware.umbrella.user.Paths.NOTIFY;
|
||||
import static de.srsoftware.umbrella.user.Paths.VALIDATE_TOKEN;
|
||||
import static java.lang.Long.parseLong;
|
||||
import static java.lang.System.Logger.Level.*;
|
||||
import static java.lang.System.Logger.Level.DEBUG;
|
||||
import static java.net.HttpURLConnection.*;
|
||||
import static java.time.temporal.ChronoUnit.DAYS;
|
||||
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import de.srsoftware.configuration.Configuration;
|
||||
import de.srsoftware.tools.Path;
|
||||
@@ -7,39 +22,36 @@ import de.srsoftware.tools.SessionToken;
|
||||
import de.srsoftware.umbrella.core.BaseHandler;
|
||||
import de.srsoftware.umbrella.core.Token;
|
||||
import de.srsoftware.umbrella.core.UmbrellaException;
|
||||
import de.srsoftware.umbrella.user.model.Session;
|
||||
import de.srsoftware.umbrella.user.model.UmbrellaUser;
|
||||
import de.srsoftware.umbrella.user.sqlite.SqliteDB;
|
||||
|
||||
import static de.srsoftware.tools.MimeType.MIME_FORM_URL;
|
||||
import static de.srsoftware.umbrella.core.ResponseCode.HTTP_SERVER_ERROR;
|
||||
import static de.srsoftware.umbrella.core.Constants.*;
|
||||
import static de.srsoftware.umbrella.core.Constants.KEY;
|
||||
import static de.srsoftware.umbrella.core.Paths.LOGOUT;
|
||||
import static de.srsoftware.umbrella.core.Paths.SEARCH;
|
||||
import static de.srsoftware.umbrella.core.Util.request;
|
||||
import static java.net.HttpURLConnection.*;
|
||||
import static java.time.temporal.ChronoUnit.DAYS;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
import org.json.JSONObject;
|
||||
|
||||
public class LegacyApi extends BaseHandler {
|
||||
|
||||
private final SqliteDB users;
|
||||
private final Configuration config;
|
||||
private final String messageUrl;
|
||||
|
||||
public LegacyApi(SqliteDB userDb, Configuration config) {
|
||||
users = userDb;
|
||||
this.config = config.subset("umbrella.modules").orElseThrow(() -> new RuntimeException("Missing configuration: umbrella.modules"));
|
||||
this.messageUrl = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean doGet(Path path, HttpExchange ex) throws IOException {
|
||||
var head = path.pop();
|
||||
return switch (head){
|
||||
case null -> sendRedirect(ex, url(ex).replaceAll("/api/.*",""));
|
||||
case "common_templates" -> {
|
||||
case null -> sendRedirect(ex, url(ex).replaceAll("/legacy/.*",""));
|
||||
case COMMON_TEMPLATES -> {
|
||||
allowOrigin(ex, "*"); // add CORS header
|
||||
yield load(path,ex);
|
||||
}
|
||||
@@ -109,7 +121,7 @@ public class LegacyApi extends BaseHandler {
|
||||
var token = SessionToken.from(ex).map(SessionToken::sessionId)
|
||||
.or(() -> getBearer(ex))
|
||||
.map(Token::of);
|
||||
if (token.isEmpty()) return sendEmptyResponse(HTTP_UNAUTHORIZED,ex);
|
||||
if (token.isEmpty()) return sendRedirect(ex,"/user/login?returnTo="+url);
|
||||
return sendRedirect(ex, url + (url.contains("?") ? "&" : "?") + "token=" + token.get());
|
||||
}
|
||||
|
||||
@@ -121,6 +133,162 @@ public class LegacyApi extends BaseHandler {
|
||||
|
||||
@Override
|
||||
public boolean doPost(Path path, HttpExchange ex) throws IOException {
|
||||
return super.doPost(path, ex);
|
||||
try {
|
||||
return switch (path.pop()) {
|
||||
case JSON -> legacyJson(ex);
|
||||
case NOTIFY -> legacyNotify(ex);
|
||||
case VALIDATE_TOKEN -> validateToken(ex);
|
||||
default -> super.doPost(path,ex);
|
||||
};
|
||||
} catch (UmbrellaException e){
|
||||
return send(ex,e);
|
||||
}
|
||||
}
|
||||
|
||||
private static String stripTrailingSlash(Object o){
|
||||
String url = o.toString();
|
||||
if (url.endsWith("/")) return url.substring(0,url.length()-1);
|
||||
return url;
|
||||
}
|
||||
|
||||
private boolean validateToken(HttpExchange ex) throws UmbrellaException, IOException {
|
||||
String body;
|
||||
try {
|
||||
body = body(ex);
|
||||
} catch (Exception e){
|
||||
throw new UmbrellaException(400,"Failed to read request body").causedBy(e);
|
||||
}
|
||||
var map = decode(body);
|
||||
LOG.log(DEBUG,"validateToken(…, {0}), data: {1}",map);
|
||||
|
||||
String domain = stripTrailingSlash(map.get(DOMAIN) instanceof String s ? s : "");
|
||||
var keys = config.keys();
|
||||
var match = false;
|
||||
for (var key : keys){
|
||||
var baseUrl = config.get(key + ".baseUrl").map(LegacyApi::stripTrailingSlash).orElse(null);
|
||||
if (domain.equals(baseUrl)){
|
||||
match = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!match) throw new UmbrellaException(500,"Failed to verify request domain!");
|
||||
|
||||
var o = map.get(TOKEN);
|
||||
if (!(o instanceof String token)) throw new UmbrellaException(500,"Request did not contain token!");
|
||||
var session = users.load(Token.of(token));
|
||||
var user = users.load(session);
|
||||
var userMap = user.toMap();
|
||||
userMap.put(TOKEN,Map.of(TOKEN,token,EXPIRATION,session.expiration().getEpochSecond()));
|
||||
return sendContent(ex,userMap);
|
||||
}
|
||||
|
||||
private boolean legacyJson(HttpExchange ex) throws UmbrellaException, IOException {
|
||||
Map<String, Object> data = null;
|
||||
try {
|
||||
data = decode(body(ex));
|
||||
} catch (IOException e) {
|
||||
LOG.log(ERROR,"Failed to extract body of request");
|
||||
throw new UmbrellaException(400,"Failed to extract body of request").causedBy(e);
|
||||
}
|
||||
var arrayPassed = false;
|
||||
var ids = new ArrayList<Long>();
|
||||
for (var entry : data.entrySet()){
|
||||
var key = entry.getKey();
|
||||
var val = entry.getValue();
|
||||
if (key.startsWith("ids[") && (arrayPassed = true)) ids.add(parseLong(entry.getValue().toString()));
|
||||
if ("ids".equals(key) && val instanceof Map<?,?> idMap) {
|
||||
for (var o : idMap.values()){
|
||||
ids.add(Long.parseLong(o.toString()));
|
||||
}
|
||||
arrayPassed = true;
|
||||
}
|
||||
}
|
||||
if (ids.isEmpty() && data.get("id") instanceof String idString) ids.add(parseLong(idString));
|
||||
var related = "true".equals(data.get("related"));
|
||||
if (related) {
|
||||
LOG.log(WARNING,"Fetching related users not implemented, yet!");
|
||||
throw new UmbrellaException(400,"Fetching related users not implemented, yet!");
|
||||
}
|
||||
|
||||
List<UmbrellaUser> userList = new ArrayList<>();
|
||||
if (ids.isEmpty()) {
|
||||
userList = users.list(0, null);
|
||||
} else {
|
||||
for (var id : ids){
|
||||
try {
|
||||
userList.add(users.load(id));
|
||||
} catch (UmbrellaException ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (arrayPassed || userList.size() != 1) {
|
||||
Map<Long, Map<String, Object>> userData = userList.stream().collect(Collectors.toMap(UmbrellaUser::id, UmbrellaUser::toMap));
|
||||
return sendContent(ex,userData);
|
||||
}
|
||||
return sendContent(ex, userList.getFirst().toMap());
|
||||
}
|
||||
|
||||
protected Session requestSession(Token token) throws UmbrellaException {
|
||||
var session = users.load(token);
|
||||
session = users.extend(session);
|
||||
return session;
|
||||
}
|
||||
|
||||
private boolean legacyNotify(HttpExchange ex) throws UmbrellaException, IOException {
|
||||
if (messageUrl == null) throw new UmbrellaException(500,"missing configuration: umbrella.modules.message.baseUrl");
|
||||
var mime = contentType(ex).orElse(null);
|
||||
JSONObject data;
|
||||
try {
|
||||
data = switch (mime) {
|
||||
case MIME_FORM_URL -> new JSONObject(decode(body(ex)));
|
||||
case null, default -> json(ex);
|
||||
};
|
||||
} catch (Exception e){
|
||||
throw new UmbrellaException(400,"Failed to fetch content of request").causedBy(e);
|
||||
}
|
||||
if (!data.has(TOKEN)) throw new UmbrellaException(400,"token missing in request data!");
|
||||
var dropKeys = new HashSet<String>();
|
||||
dropKeys.add(TOKEN);
|
||||
var token = Token.of(data.getString(TOKEN));
|
||||
var userSession = requestSession(token);
|
||||
var user = userSession.user();
|
||||
LOG.log(DEBUG,"Token belongs to {0}.",user);
|
||||
|
||||
// recipients is a field used by legacy code.
|
||||
// it may be a single user id or a list of user ids.
|
||||
var recipients = new HashSet<Long>();
|
||||
for (var key : data.keySet()){
|
||||
if (key.startsWith("recipients[")) {
|
||||
recipients.add(data.getLong(key));
|
||||
dropKeys.add(key);
|
||||
break;
|
||||
}
|
||||
if (key.equals("recipients")){
|
||||
var list = data.getJSONObject(key);
|
||||
for (var idx : list.keySet()){
|
||||
var id = list.getLong(idx);
|
||||
recipients.add(id);
|
||||
}
|
||||
dropKeys.add(key);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!recipients.isEmpty()){ // replace legacy user ids by user objects in receivers field
|
||||
List<UmbrellaUser> resp = users.list(0, null, recipients.toArray(new Long[0]));
|
||||
data.put("receivers",resp.stream().map(UmbrellaUser::toMap).toList());
|
||||
}
|
||||
|
||||
if (!data.has(SENDER)) data.put(SENDER,user.toMap());
|
||||
|
||||
dropKeys.forEach(data::remove);
|
||||
|
||||
LOG.log(DEBUG,"received legacy notification: {0}",data);
|
||||
|
||||
var messageList = Map.of("messages",List.of(data.toMap()));
|
||||
var resp = request(messageUrl, messageList, null, token.asBearer());
|
||||
if (!(resp instanceof JSONObject json)) throw new UmbrellaException(500,"{0} did not return JSON!",messageUrl);
|
||||
|
||||
// TODO: should we return json?
|
||||
return sendEmptyResponse(HTTP_OK,ex);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user