Add user and captcha handling methods

This commit is contained in:
netkas 2024-12-09 19:03:33 -05:00
parent 23ac46eee8
commit 69b68ed1e4
9 changed files with 854 additions and 436 deletions

View file

@ -8,6 +8,8 @@ import net.nosial.socialclient.exceptions.CryptographyException;
import net.nosial.socialclient.exceptions.ResolutionException; import net.nosial.socialclient.exceptions.ResolutionException;
import net.nosial.socialclient.exceptions.RpcException; import net.nosial.socialclient.exceptions.RpcException;
import net.nosial.socialclient.objects.RpcRequest; import net.nosial.socialclient.objects.RpcRequest;
import net.nosial.socialclient.objects.standard.ImageCaptcha;
import net.nosial.socialclient.objects.standard.SelfUser;
import java.security.KeyPair; import java.security.KeyPair;
import java.security.PrivateKey; import java.security.PrivateKey;
@ -17,8 +19,6 @@ import java.util.Map;
public class Client extends RpcClient public class Client extends RpcClient
{ {
public Client(String domain) throws ResolutionException, CryptographyException public Client(String domain) throws ResolutionException, CryptographyException
{ {
super(domain); super(domain);
@ -31,7 +31,8 @@ public class Client extends RpcClient
public String createSession(KeyPair keyPair) throws RpcException public String createSession(KeyPair keyPair) throws RpcException
{ {
RpcResult response = this.sendRequest(new RpcRequest("create_session", Utilities.randomCrc32(), new HashMap<>(){{ RpcResult response = this.sendRequest(new RpcRequest("createSession", Utilities.randomCrc32(), new HashMap<>()
{{
put("public_key", Cryptography.exportPublicKey(keyPair)); put("public_key", Cryptography.exportPublicKey(keyPair));
}})); }}));
@ -78,4 +79,90 @@ public class Client extends RpcClient
return true; return true;
} }
public boolean register(String username) throws RpcException
{
RpcResult response = this.sendRequest(new RpcRequest("register", Utilities.randomCrc32(), new HashMap<>()
{{
put("username", username);
}}));
if(response == null)
{
throw new RpcException("Response is null");
}
if(!response.isSuccess())
{
throw new RpcException(response);
}
return (boolean)response.getResponse().getResult();
}
@SuppressWarnings("unchecked")
public SelfUser getMe() throws RpcException
{
RpcResult response = this.sendRequest(new RpcRequest("getMe", Utilities.randomCrc32()));
if(response == null)
{
throw new RpcException("Response is null");
}
if(!response.isSuccess())
{
throw new RpcException(response);
}
if(response.getResponse().getResult() instanceof Map<?,?>)
{
return new SelfUser((Map<String, Object>) response.getResponse().getResult());
}
throw new RpcException("Unexpected result type, expected Map<?,?>");
}
@SuppressWarnings("unchecked")
public ImageCaptcha verificationGetImageCaptcha() throws RpcException
{
RpcResult response = this.sendRequest(new RpcRequest("verificationGetImageCaptcha", Utilities.randomCrc32()));
if(response == null)
{
throw new RpcException("Response is null");
}
if(!response.isSuccess())
{
throw new RpcException(response);
}
if(response.getResponse().getResult() instanceof Map<?,?>)
{
return new ImageCaptcha((Map<String, Object>) response.getResponse().getResult());
}
throw new RpcException("Unexpected result type, expected Map<?,?>");
}
public boolean verificationAnswerImageCaptcha(String answer) throws RpcException
{
RpcResult response = this.sendRequest(new RpcRequest("verificationAnswerImageCaptcha", Utilities.randomCrc32(), new HashMap<>()
{{
put("answer", answer);
}}));
if(response == null)
{
throw new RpcException("Response is null");
}
if(!response.isSuccess())
{
throw new RpcException(response);
}
return (boolean)response.getResponse().getResult();
}
} }

View file

@ -9,8 +9,10 @@ import java.util.Hashtable;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
public class Resolver { public class Resolver
public static ResolvedServer resolveDomain(String domain) throws ResolutionException { {
public static ResolvedServer resolveDomain(String domain) throws ResolutionException
{
final Pattern fullPattern = Pattern.compile("v=socialbox;sb-rpc=(https?://[^;]+);sb-key=([^;]+)"); final Pattern fullPattern = Pattern.compile("v=socialbox;sb-rpc=(https?://[^;]+);sb-key=([^;]+)");
Hashtable<String, String> env = new Hashtable<>(); Hashtable<String, String> env = new Hashtable<>();
@ -18,18 +20,21 @@ public class Resolver {
env.put("com.sun.jndi.dns.cache.ttl", "1"); env.put("com.sun.jndi.dns.cache.ttl", "1");
env.put("com.sun.jndi.dns.cache.negative.ttl", "0"); env.put("com.sun.jndi.dns.cache.negative.ttl", "0");
try { try
{
DirContext dirContext = new InitialDirContext(env); DirContext dirContext = new InitialDirContext(env);
Attributes attributes = dirContext.getAttributes("dns:/" + domain, new String[]{"TXT"}); Attributes attributes = dirContext.getAttributes("dns:/" + domain, new String[]{"TXT"});
Attribute attributeTXT = attributes.get("TXT"); Attribute attributeTXT = attributes.get("TXT");
if (attributeTXT == null) { if (attributeTXT == null)
{
throw new ResolutionException("Failed to resolve DNS TXT records for " + domain); throw new ResolutionException("Failed to resolve DNS TXT records for " + domain);
} }
StringBuilder fullRecordBuilder = new StringBuilder(); StringBuilder fullRecordBuilder = new StringBuilder();
for (int i = 0; i < attributeTXT.size(); i++) { for (int i = 0; i < attributeTXT.size(); i++)
{
String value = (String) attributeTXT.get(i); String value = (String) attributeTXT.get(i);
fullRecordBuilder.append(value.replaceAll("^\"|\"$", "").trim()); fullRecordBuilder.append(value.replaceAll("^\"|\"$", "").trim());
} }
@ -37,23 +42,30 @@ public class Resolver {
String fullRecord = fullRecordBuilder.toString(); String fullRecord = fullRecordBuilder.toString();
Matcher matcher = fullPattern.matcher(fullRecord); Matcher matcher = fullPattern.matcher(fullRecord);
if (matcher.find()) { if (matcher.find())
{
String endpoint = matcher.group(1); String endpoint = matcher.group(1);
String publicKey = matcher.group(2).replaceAll("\\s+", ""); String publicKey = matcher.group(2).replaceAll("\\s+", "");
if (endpoint == null || endpoint.isEmpty()) { if (endpoint == null || endpoint.isEmpty())
{
throw new ResolutionException("Failed to resolve RPC endpoint for " + domain); throw new ResolutionException("Failed to resolve RPC endpoint for " + domain);
} }
if (publicKey == null || publicKey.isEmpty()) { if (publicKey == null || publicKey.isEmpty())
{
throw new ResolutionException("Failed to resolve public key for " + domain); throw new ResolutionException("Failed to resolve public key for " + domain);
} }
return new ResolvedServer(endpoint, publicKey); return new ResolvedServer(endpoint, publicKey);
} else { }
else
{
throw new ResolutionException("Failed to find valid SocialBox record for " + domain); throw new ResolutionException("Failed to find valid SocialBox record for " + domain);
} }
} catch (NamingException e) { }
catch (NamingException e)
{
throw new ResolutionException("Error resolving domain: " + e.getMessage(), e); throw new ResolutionException("Error resolving domain: " + e.getMessage(), e);
} }
} }

View file

@ -1,26 +1,26 @@
package net.nosial.socialclient.classes; package net.nosial.socialclient.classes;
import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.squareup.okhttp.*; import com.squareup.okhttp.*;
import net.nosial.socialclient.abstracts.RpcResult; import net.nosial.socialclient.abstracts.RpcResult;
import net.nosial.socialclient.exceptions.CryptographyException; import net.nosial.socialclient.exceptions.CryptographyException;
import net.nosial.socialclient.exceptions.ResolutionException; import net.nosial.socialclient.exceptions.ResolutionException;
import net.nosial.socialclient.objects.ResolvedServer; import net.nosial.socialclient.objects.ResolvedServer;
import net.nosial.socialclient.objects.RpcRequest; import net.nosial.socialclient.objects.RpcRequest;
import java.io.IOException; import java.io.IOException;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey; import java.security.PrivateKey;
import java.security.PublicKey; import java.security.PublicKey;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
public class RpcClient public class RpcClient
{ {
private final static ObjectMapper OBJECT_MAPPER = new ObjectMapper(); private final static ObjectMapper OBJECT_MAPPER = new ObjectMapper();
private final static MediaType MEDIA_TYPE_JSON = MediaType.parse("application/json; charset=utf-8"); private final static MediaType MEDIA_TYPE_JSON = MediaType.parse("application/json; charset=utf-8");
@ -30,7 +30,7 @@ public class RpcClient
private final OkHttpClient httpClient; private final OkHttpClient httpClient;
private String clientName = "SocialClient Java"; private String clientName = "SocialClient Java";
private String clientVersion = "1.0.0"; private String clientVersion = "1.0";
protected String sessionUuid; protected String sessionUuid;
protected PrivateKey privateKey; protected PrivateKey privateKey;
@ -226,11 +226,9 @@ public class RpcClient
try try
{ {
// NOTE: Signature looks okay, it's base64 encoded. // NOTE: Signature looks okay, it's base64 encoded.
System.out.println("Hash: " + Cryptography.sha1(jsonData));
System.out.println("Signature: " + Cryptography.signContent(jsonData, privateKey));
this.addHeader("Signature", Cryptography.signContent(jsonData, privateKey)); this.addHeader("Signature", Cryptography.signContent(jsonData, privateKey));
} }
catch(CryptographyException | NoSuchAlgorithmException e) catch(CryptographyException e)
{ {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
@ -258,6 +256,11 @@ public class RpcClient
return new ArrayList<>(); return new ArrayList<>();
} }
if(responseString.isEmpty())
{
throw new RuntimeException("The request was successful but the server did not indicate a empty response");
}
Object decoded = decode(responseString); Object decoded = decode(responseString);
// Singular object response // Singular object response
@ -420,4 +423,4 @@ public class RpcClient
throw new RuntimeException("Failed to encode input to JSON Data", e); throw new RuntimeException("Failed to encode input to JSON Data", e);
} }
} }
} }

View file

@ -11,8 +11,15 @@ public enum StandardErrorCodes
SERVER_UNAVAILABLE(-2001), SERVER_UNAVAILABLE(-2001),
INVALID_PUBLIC_KEY(-3000), INVALID_PUBLIC_KEY(-3000),
SESSION_NOT_FOUND(-3001), UNSUPPORTED_AUTHENTICATION_TYPE(-3001),
UNSUPPORTED_AUTHENTICATION_TYPE(-3002); ALREADY_AUTHENTICATED(-3002),
AUTHENTICATION_REQUIRED(-3003),
SESSION_NOT_FOUND(-3004),
SESSION_REQUIRED(-3005),
PEER_NOT_FOUND(-4000),
INVALID_USERNAME(-4001),
USERNAME_ALREADY_EXISTS(-4002);
private final int code; private final int code;

View file

@ -0,0 +1,39 @@
package net.nosial.socialclient.enums.flags;
public enum PeerFlags
{
// Administrative Flags
ADMIN(true, "ADMIN"),
MODERATOR(true, "MODERATOR"),
// General Flags
VERIFIED(true, "VERIFIED"),
// Verification Flags
VER_SET_PASSWORD(false, "VER_SET_PASSWORD"),
VER_SET_OTP(false, "VER_SET_OTP"),
VER_SET_DISPLAY_NAME(false, "VER_SET_DISPLAY_NAME"),
VER_EMAIL(false, "VER_EMAIL"),
VER_SMS(false, "VER_SMS"),
VER_PHONE_CALL(false, "VER_PHONE_CALL"),
VER_SOLVE_IMAGE_CAPTCHA(false, "VER_SOLVE_IMAGE_CAPTCHA");
private final boolean isPublic;
private final String flag;
PeerFlags(boolean isPublic, String flag)
{
this.isPublic = isPublic;
this.flag = flag;
}
public String getFlag()
{
return this.flag;
}
public boolean isPublic()
{
return this.isPublic;
}
}

View file

@ -0,0 +1,46 @@
package net.nosial.socialclient.objects.standard;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.Base64;
import java.util.Map;
public class ImageCaptcha
{
private final int expires;
private final String image;
public ImageCaptcha(Map<String, Object> data)
{
this.expires = (int) data.get("expires");
this.image = (String) data.get("image");
}
public int getExpires()
{
return expires;
}
public String getImage()
{
return image;
}
public BufferedImage getImageBuffer() throws IOException
{
String base64Image = this.image;
if(this.image.contains(","))
{
base64Image = this.image.split(",")[1];
}
byte[] imageBytes = Base64.getDecoder().decode(base64Image);
ByteArrayInputStream bis = new ByteArrayInputStream(imageBytes);
BufferedImage image = javax.imageio.ImageIO.read(bis);
bis.close();
return image;
}
}

View file

@ -0,0 +1,106 @@
package net.nosial.socialclient.objects.standard;
import net.nosial.socialclient.enums.flags.PeerFlags;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class SelfUser
{
private final String uuid;
private final boolean enabled;
private final String address;
private final String username;
private final String displayName;
private final List<PeerFlags> flags;
private final int created;
/**
* Constructs a SelfUser instance by populating fields from a provided data map.
*
* @param data a map containing user details such as uuid, username, display name, flags,
* enabled status, and creation time.
*/
@SuppressWarnings("unchecked")
public SelfUser(Map<String, Object> data)
{
this.uuid = (String) data.get("uuid");
this.enabled = (boolean) data.get("enabled");
this.username = (String) data.get("username");
this.address = (String) data.get("address");
this.displayName = (String) data.get("display_name");
this.flags = ((List<String>) data.get("flags")).stream().map(PeerFlags::valueOf).collect(Collectors.toList());
this.created = (int) data.get("created");
}
/**
* Retrieves the universally unique identifier (UUID) associated with the user.
*
* @return the UUID as a String
*/
public String getUuid()
{
return uuid;
}
/**
* Checks if the user account is enabled.
*
* @return true if the user account is enabled; false otherwise.
*/
public boolean isEnabled()
{
return enabled;
}
/**
* Retrieves the username associated with this SelfUser instance.
*
* @return the username of the user
*/
public String getUsername()
{
return username;
}
/**
* Retrieves the address associated with this SelfUser instance.
*
* @return the address of the user
*/
public String getAddress()
{
return address;
}
/**
* Retrieves the display name of the user.
*
* @return the display name of the user
*/
public String getDisplayName()
{
return displayName;
}
/**
* Retrieves the list of flags associated with the user.
*
* @return a list of PeerFlags indicating various user permissions and statuses.
*/
public List<PeerFlags> getFlags()
{
return flags;
}
/**
* Returns the creation timestamp of this user.
*
* @return the creation timestamp as an integer
*/
public int getCreated()
{
return created;
}
}

View file

@ -1,5 +1,6 @@
package net.nosial.socialclient; package net.nosial.socialclient.methods;
import net.nosial.socialclient.Client;
import net.nosial.socialclient.classes.Cryptography; import net.nosial.socialclient.classes.Cryptography;
import net.nosial.socialclient.exceptions.CryptographyException; import net.nosial.socialclient.exceptions.CryptographyException;
import net.nosial.socialclient.exceptions.ResolutionException; import net.nosial.socialclient.exceptions.ResolutionException;
@ -10,11 +11,10 @@ import java.security.KeyPair;
import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.*;
class SessionTest public class CreateSessionTest
{ {
@Test @Test
void createSessionTest() void testSessionCreation()
{ {
Client socialClient; Client socialClient;
KeyPair keyPair; KeyPair keyPair;
@ -23,18 +23,9 @@ class SessionTest
try try
{ {
socialClient = new Client("n64.cc"); socialClient = new Client("n64.cc");
}
catch (ResolutionException | CryptographyException e)
{
fail(e.getMessage(), e);
return;
}
try
{
keyPair = Cryptography.generateKeyPair(); keyPair = Cryptography.generateKeyPair();
} }
catch (CryptographyException e) catch (ResolutionException | CryptographyException e)
{ {
fail(e.getMessage(), e); fail(e.getMessage(), e);
return; return;

View file

@ -0,0 +1,127 @@
package net.nosial.socialclient.methods;
import net.nosial.socialclient.Client;
import net.nosial.socialclient.classes.Cryptography;
import net.nosial.socialclient.exceptions.CryptographyException;
import net.nosial.socialclient.exceptions.ResolutionException;
import net.nosial.socialclient.exceptions.RpcException;
import net.nosial.socialclient.objects.standard.ImageCaptcha;
import net.nosial.socialclient.objects.standard.SelfUser;
import org.junit.jupiter.api.Test;
import javax.swing.*;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.security.KeyPair;
import java.util.Random;
import static org.junit.jupiter.api.Assertions.*;
public class RegisterTest
{
@Test
public void registerTest()
{
// Step 1. Connect and establish a session to the server
final Client socialClient;
final KeyPair keyPair;
final String sessionUuid;
try
{
socialClient = new Client("n64.cc");
keyPair = Cryptography.generateKeyPair();
sessionUuid = socialClient.createAndSetSession(keyPair);
}
catch (ResolutionException | CryptographyException | RpcException e)
{
fail(e.getMessage(), e);
return;
}
System.out.println("Session UUID: " + sessionUuid);
assertNotNull(sessionUuid);
final SelfUser selfUser;
Random random = new Random();
String username = "user" + random.nextInt(10000);
// Step 2. Register the username and getSelf
try
{
assertTrue(socialClient.register(username));
selfUser = socialClient.getMe();
}
catch (RpcException e)
{
fail(e.getMessage(), e);
return;
}
assertNotNull(selfUser);
// Step 3. Get the captcha
ImageCaptcha captcha;
try
{
captcha = socialClient.verificationGetImageCaptcha();
}
catch(RpcException e)
{
fail(e.getMessage(), e);
return;
}
SwingUtilities.invokeLater(() -> {
try
{
ImageFrame frame = new ImageFrame(captcha.getImageBuffer());
frame.setVisible(true);
}
catch(IOException e)
{
e.printStackTrace();
}
});
assertNotNull(captcha);
// Step 4. Verify the captcha, get input from the user
String captchaInput = JOptionPane.showInputDialog("Enter the captcha: ");
assertNotNull(captchaInput);
// Step 5. Verify the captcha
try
{
assertTrue(socialClient.verificationAnswerImageCaptcha(captchaInput));
}
catch(RpcException e)
{
fail(e.getMessage(), e);
return;
}
}
private static class ImageFrame extends JFrame {
public ImageFrame(BufferedImage image) {
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setResizable(false);
JPanel panel = new JPanel() {
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
g.drawImage(image, 0, 0, null);
}
};
panel.setPreferredSize(new Dimension(image.getWidth(), image.getHeight()));
add(panel);
pack();
setLocationRelativeTo(null);
}
}
}