ported legacy functions

This commit is contained in:
2025-07-04 14:00:46 +02:00
parent e48ddfdb2c
commit 3c898f36de
32 changed files with 2642 additions and 35 deletions

View File

@@ -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);
}
}