Skip to content

Commit

Permalink
Improve contacts view
Browse files Browse the repository at this point in the history
Refresh contacts automatically when they're added or removed.

Make profiles display their creation time.

Improve chat room text input layout (no longer mangles the text height, better margins).

Make contacts display to which profile they're linked to.

Fix errors being displayed when fetching a non existing media entry.

Add identity validation for PGP signed profiles.

Fix hash generation for signed identities. The local generated identity is automatically fixed.

Fix PGP id hexadecimal representation. Luckily this didn't cause any problem (other than display issues) because it wasn't being used yet.

Add support for service strings.

Add support for vote values for validated identities.

Remove MacOS from the list of supported platforms because I don't have a way to test it. See #172
  • Loading branch information
zapek committed Oct 13, 2024
1 parent 60a641a commit aade068
Show file tree
Hide file tree
Showing 52 changed files with 1,129 additions and 271 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
- 🖥️ Modern looking desktop user interface with several themes including dark mode
- 📶 Remote access
- 📖 Free software ([GPL](https://www.gnu.org/licenses/quick-guide-gplv3.html))
- 😃 Available for Windows, Linux and MacOS
- 😃 Available for Windows and Linux

## Releases

Expand Down
14 changes: 14 additions & 0 deletions app/src/main/java/io/xeres/app/api/DefaultHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.context.request.async.AsyncRequestNotUsableException;
import org.springframework.web.server.ResponseStatusException;

import java.net.UnknownHostException;
import java.util.NoSuchElementException;
Expand Down Expand Up @@ -93,6 +94,19 @@ public ResponseEntity<Error> handleRuntimeException(RuntimeException e)
.build();
}

/**
* Generates a ResponseStatusException. Those are typically done from media endpoints
* and there's no way to put JSON error messages in there, so just ignore them.
*
* @param e the exception
* @return a ResponseEntity with just the status code and no message
*/
@ExceptionHandler(ResponseStatusException.class)
public ResponseEntity<Void> handleResponseStatusException(ResponseStatusException e)
{
return new ResponseEntity<>(e.getStatusCode());
}

@ExceptionHandler(AsyncRequestNotUsableException.class)
public void handleAsyncRequestNotUsableException(AsyncRequestNotUsableException ignored)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;

import java.io.ByteArrayInputStream;
Expand Down Expand Up @@ -77,7 +78,7 @@ public IdentityDTO findIdentityById(@PathVariable long id)
@ApiResponse(responseCode = "404", description = "Identity not found", content = @Content(schema = @Schema(implementation = Error.class)))
public ResponseEntity<InputStreamResource> downloadIdentityImage(@PathVariable long id)
{
var identity = identityRsService.findById(id).orElseThrow();
var identity = identityRsService.findById(id).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); // Bypass the global controller advice because it only knows about application/json mimetype
var imageType = ImageDetectionUtils.getImageMimeType(identity.getImage());
if (imageType == null)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,11 @@
import io.xeres.common.rest.Error;
import io.xeres.common.rest.location.RSIdResponse;
import io.xeres.common.rsid.Type;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ResponseStatusException;

import java.awt.image.BufferedImage;

Expand Down Expand Up @@ -81,7 +83,7 @@ public RSIdResponse getRSIdOfLocation(@PathVariable long id, @RequestParam(value
@ApiResponse(responseCode = "404", description = "Profile not found", content = @Content(schema = @Schema(implementation = Error.class)))
public ResponseEntity<BufferedImage> getRSIdOfLocationAsQrCode(@PathVariable long id)
{
var location = locationService.findLocationById(id).orElseThrow();
var location = locationService.findLocationById(id).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND)); // Bypass the global controller advice because it only knows about application/json mimetype

return ResponseEntity.ok(qrCodeService.generateQrCode(location.getRsId(Type.SHORT_INVITE).getArmored()));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.xeres.app.service.notification.contact.ContactNotificationService;
import io.xeres.app.service.notification.file.FileNotificationService;
import io.xeres.app.service.notification.file.FileSearchNotificationService;
import io.xeres.app.service.notification.forum.ForumNotificationService;
Expand All @@ -44,13 +45,15 @@ public class NotificationController
private final ForumNotificationService forumNotificationService;
private final FileNotificationService fileNotificationService;
private final FileSearchNotificationService fileSearchNotificationService;
private final ContactNotificationService contactNotificationService;

public NotificationController(StatusNotificationService statusNotificationService, ForumNotificationService forumNotificationService, FileNotificationService fileNotificationService, FileSearchNotificationService fileSearchNotificationService)
public NotificationController(StatusNotificationService statusNotificationService, ForumNotificationService forumNotificationService, FileNotificationService fileNotificationService, FileSearchNotificationService fileSearchNotificationService, ContactNotificationService contactNotificationService)
{
this.statusNotificationService = statusNotificationService;
this.forumNotificationService = forumNotificationService;
this.fileNotificationService = fileNotificationService;
this.fileSearchNotificationService = fileSearchNotificationService;
this.contactNotificationService = contactNotificationService;
}

@GetMapping("/status")
Expand Down Expand Up @@ -84,4 +87,12 @@ public SseEmitter setupFileSearchNotification()
{
return fileSearchNotificationService.addClient();
}

@GetMapping("/contact")
@Operation(summary = "Subscribe to contact notifications")
@ApiResponse(responseCode = "200", description = "Request completed successfully")
public SseEmitter setupContactNotification()
{
return contactNotificationService.addClient();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ public ResponseEntity<Void> createProfileFromRsId(@Valid @RequestBody RsIdReques
profile.setTrust(trust);
}

var savedProfile = profileService.createOrUpdateProfile(profile).orElseThrow(() -> new UnprocessableEntityException("Failed to save profile"));
var savedProfile = profileService.createOrUpdateProfile(profile);

statusNotificationService.incrementTotalUsers(); // not correct if a certificate is used to update an existing profile (XXX: put a created date? or age? that would detect it)

Expand Down
68 changes: 4 additions & 64 deletions app/src/main/java/io/xeres/app/application/Startup.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,20 +27,15 @@
import io.xeres.app.configuration.DataDirConfiguration;
import io.xeres.app.database.DatabaseSession;
import io.xeres.app.database.DatabaseSessionManager;
import io.xeres.app.database.model.file.File;
import io.xeres.app.database.model.settings.Settings;
import io.xeres.app.database.model.share.Share;
import io.xeres.app.net.peer.PeerConnectionManager;
import io.xeres.app.service.*;
import io.xeres.app.service.file.FileService;
import io.xeres.app.service.notification.file.FileNotificationService;
import io.xeres.app.service.notification.status.StatusNotificationService;
import io.xeres.app.xrs.service.identity.IdentityManager;
import io.xeres.common.AppName;
import io.xeres.common.events.StartupEvent;
import io.xeres.common.mui.MinimalUserInterface;
import io.xeres.common.pgp.Trust;
import io.xeres.common.util.SecureRandomUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.ApplicationArguments;
Expand All @@ -50,12 +45,8 @@
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;

@Component
public class Startup implements ApplicationRunner
Expand All @@ -72,13 +63,13 @@ public class Startup implements ApplicationRunner
private final IdentityManager identityManager;
private final StatusNotificationService statusNotificationService;
private final AutoStart autoStart;
private final FileService fileService;
private final ShellService shellService;
private final FileNotificationService fileNotificationService;
private final InfoService infoService;
private final UpgradeService upgradeService;
private final ApplicationEventPublisher publisher;

public Startup(LocationService locationService, SettingsService settingsService, DatabaseSessionManager databaseSessionManager, DataDirConfiguration dataDirConfiguration, NetworkService networkService, PeerConnectionManager peerConnectionManager, UiBridgeService uiBridgeService, IdentityManager identityManager, StatusNotificationService statusNotificationService, AutoStart autoStart, FileService fileService, ShellService shellService, FileNotificationService fileNotificationService, InfoService infoService, ApplicationEventPublisher publisher)
public Startup(LocationService locationService, SettingsService settingsService, DatabaseSessionManager databaseSessionManager, DataDirConfiguration dataDirConfiguration, NetworkService networkService, PeerConnectionManager peerConnectionManager, UiBridgeService uiBridgeService, IdentityManager identityManager, StatusNotificationService statusNotificationService, AutoStart autoStart, ShellService shellService, FileNotificationService fileNotificationService, InfoService infoService, UpgradeService upgradeService, ApplicationEventPublisher publisher)
{
this.locationService = locationService;
this.settingsService = settingsService;
Expand All @@ -90,10 +81,10 @@ public Startup(LocationService locationService, SettingsService settingsService,
this.identityManager = identityManager;
this.statusNotificationService = statusNotificationService;
this.autoStart = autoStart;
this.fileService = fileService;
this.shellService = shellService;
this.fileNotificationService = fileNotificationService;
this.infoService = infoService;
this.upgradeService = upgradeService;
this.publisher = publisher;
}

Expand All @@ -117,7 +108,7 @@ public void run(ApplicationArguments args)
return;
}

configureDefaults();
upgradeService.upgrade();

if (networkService.checkReadiness())
{
Expand Down Expand Up @@ -229,55 +220,4 @@ private void syncAutoStart()
}
}
}

/**
* Configures defaults that cannot be done on the database definition because
* they depend on some runtime parameters. This is not called in UI client
* only mode.
*/
private void configureDefaults()
{
var version = 2; // Increment this number when needing to add new defaults

// Don't do this stuff when running tests
if (dataDirConfiguration.getDataDir() == null)
{
return;
}

if (!settingsService.hasIncomingDirectory())
{
var incomingDirectory = Path.of(dataDirConfiguration.getDataDir(), "Incoming");
if (Files.notExists(incomingDirectory))
{
try
{
Files.createDirectory(incomingDirectory);
}
catch (IOException e)
{
throw new IllegalStateException("Couldn't create incoming directory: " + incomingDirectory + ", :" + e.getMessage());
}
}
settingsService.setIncomingDirectory(incomingDirectory.toString());
fileService.addShare(Share.createShare("Incoming", File.createFile(incomingDirectory), false, Trust.UNKNOWN));
}

if (settingsService.getVersion() < 1)
{
var password = new char[20];
SecureRandomUtils.nextPassword(password);
settingsService.setRemotePassword(String.valueOf(password));
Arrays.fill(password, (char) 0);
}

if (settingsService.getVersion() < 2)
{
fileService.encryptAllHashes();
}

// [Add new defaults here]

settingsService.setVersion(version);
}
}
35 changes: 27 additions & 8 deletions app/src/main/java/io/xeres/app/crypto/pgp/PGP.java
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,32 @@ public static void sign(PGPSecretKey pgpSecretKey, InputStream in, OutputStream
* @throws PGPException if there's a PGP error
*/
public static void verify(PGPPublicKey pgpPublicKey, byte[] signature, InputStream in) throws IOException, SignatureException, PGPException
{
var pgpSignature = getSignature(signature);

pgpSignature.init(new BcPGPContentVerifierBuilderProvider(), pgpPublicKey);
pgpSignature.update(in.readAllBytes());
in.close();
if (!pgpSignature.verify())
{
throw new SignatureException("Wrong signature");
}
}

public static long getIssuer(byte[] signature)
{
try
{
var pgpSignature = getSignature(signature);
return pgpSignature.getKeyID();
}
catch (SignatureException | IOException e)
{
return 0L;
}
}

private static PGPSignature getSignature(byte[] signature) throws SignatureException, IOException
{
var pgpObjectFactory = new PGPObjectFactory(signature, new BcKeyFingerprintCalculator());

Expand Down Expand Up @@ -284,14 +310,7 @@ public static void verify(PGPPublicKey pgpPublicKey, byte[] signature, InputStre
{
throw new SignatureException("Signature hash algorithm is not of SHA family (" + pgpSignature.getHashAlgorithm() + ")");
}

pgpSignature.init(new BcPGPContentVerifierBuilderProvider(), pgpPublicKey);
pgpSignature.update(in.readAllBytes());
in.close();
if (!pgpSignature.verify())
{
throw new SignatureException("Wrong signature");
}
return pgpSignature;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,10 @@ public ChatRoomBacklog(ChatRoom room, GxsId gxsId, String nickname, String messa
this.message = message;
}

public ChatRoomBacklog(ChatRoom room, String message)
public ChatRoomBacklog(ChatRoom room, String nickname, String message)
{
this.room = room;
this.nickname = nickname;
this.message = message;
}

Expand Down
Loading

0 comments on commit aade068

Please sign in to comment.