📌基础
基本类型比较
- 整数型比较
看底层实现仍然是int型比较,为什么要使用compareTo方法呢?if (Integer.valueOf(1).compareTo(number) > 0) {return R.fail("数量小于1");}
📌数据校验
校验 跨信任边界 传递的 不可信数据
信任边界
被系统直接控制的所有组件都是被系统信任的,所有来自不受控的外部系统的连接与数据,包括客户端、第三方系统,都应该被认为是不可信的,要先在边界处对其校验,才能允许它们进一步与本系统交互。
有以下四种数据校验的策略
白名单正向验证:检查数据是否属于一个严格约束的、已知的、可接受合法数据集合。以下代码确保name只包含数字、字母
if (Pattern.matches("^[0-9A-Za-z_]+$", name)){throw new IllegalArgumentException("Invalid name");}
黑名单负向验证:拒绝不合法数据,因为潜在的不合法数据可能是不受约束的无限集合,这样意味着你必须一直维护一个不合法字符或者模式的列表,需要定期更新不合法的正则表达式以及定期研究新的攻击方式,否则程序中的校验会很快过时。
- 白名单方式净化:把不属于合法字符列表的字符进行删除、替换或者编码,然后校验净化后的数据,最后使用净化后的数据。
对于一个用户评论栏的文本输入,确定一个合法的数据集合是非常困难的,因为几乎所有的字符都可以被用到。一种解决方案是:将所有非字母数字替换成其编码后的版本,那么“I like your web page!”被净化后将输出为“I+like+your+web+page%21”,这里使用了URL编码。 - 黑名单方式净化:把所有不合法字符进行剔除或转换(删除引号、转换成html实体)
禁止直接使用不可信任数据拼接SQL
组合一:使用PreparedStatement防止SQL注入
错误代码 ```java Statement stmt = null; ResultSet rs = null; try{ String userName = ctx.getAuthenticatedUserName(); //this is a constant String sqlString = “SELECT * FROM t_item WHERE owner=’” + userName + “‘ AND itemName=’” + request.getParameter(“itemName”) + “‘“; stmt = connection.createStatement(); rs = stmt.executeQuery(sqlString); // … result set handling }catch (SQLException se){ // … logging and error handling }
如果一个攻击者以用户名_wiley_发起一个请求,并使用以下条目名称参数进行查询:name' OR 'a' = 'a<br />那么这个查询将变成:SELECT * FROM t_item WHERE owner = 'wiley' AND itemname = 'name' OR 'a'='a';<br />导致where子句总为真。<br />正确代码
```java
reparedStatement stmt = null
ResultSet rs = null
tr{
String userName = ctx.getAuthenticatedUserName(); //this is a constant
String itemName = request.getParameter("itemName");
// ...Ensure that the length of userName and itemName is legitimate
// ...
String sqlString = "SELECT * FROM t_item WHERE owner=? AND itemName=?";
stmt = connection.prepareStatement(sqlString);
stmt.setString(1, userName);
stmt.setString(2, itemName);
rs = stmt.executeQuery();
// ... result set handling
}catch (SQLException se){
// ... logging and error handling
}
为什么PreparedStatement能够防止SQL注入?
- 静态SQL的拼接不会处理特殊字符,如单引号;而使用PreparedStatement能够识别SQL中包裹字符串的单引号(不会转义)和字符串中的单引号(将其转义)
禁止直接使用不可信任数据拼接XML
错误示例:
private void createXMLStream(BufferedOutputStream outStream, User user) throws IOException{
String xmlString;
xmlString = "<user><role>operator</role><id>" + user.getUserId()
+ "</id><description>" + user.getDescription() + "</description></user>";
outStream.write(xmlString.getBytes());
outStream.flush();
}
user.getUserId() = “joe
user.getDescription() = “I want to be an administrator”
最终XML字符串会变成如下形式:
<user>
<role>operator</role>
<id>joe</id>
<role>administrator</role>
<id>joe</id>
<description>I want to be an administrator</description>
</user>
由于SAX解析器(org.xml.sax and javax.xml.parsers.SAXParser)在解释XML文档时会将第二个role域的值覆盖前一个role域的值,因此导致此用户角色由操作员提升为了管理员。
禁止直接使用不可信任数据来记录日志
如果在记录的日志中包含未经校验的不可信数据,则有可能导致日志注入漏洞。恶意用户会插入伪造的日志数据,从而让系统管理员误以为这些日志数据是由系统记录的。
从格式化字符串中排除用户输入
错误示例:
class Format
{
static Calendar c = new GregorianCalendar(1995, GregorianCalendar.MAY, 23);
public static void main(String[] args)
{
// args[0] is the credit card expiration date
// args[0] may contain either %1$tm, %1$te or %1$tY as malicious arguments
// First argument prints 05 (May), second prints 23 (day)
// and third prints 1995 (year)
// Perform comparison with c, if it doesn't match print the following line
System.out.printf(args[0]
+ " did not match! HINT: It was issued on %1$terd of some month",
c);
}
}
禁止向Runtime.exec()方法传递不可信、未净化的数据
验证路径之前应该将其标准化
绝对路径名或者相对路径名中可能会包含文件链接,比如符号(软)链接(symbolic [soft] links)、硬链接(hard links)、快捷方式(shortcuts)、影子文件(shadows)、别名(aliases)与连接文件(junctions)。这些文件链接在文件验证操作进行之前必须被完全解析。
当试图限制用户只能访问某个特定目录中的文件时,或者当基于文件名或者路径名来做安全决策时。攻击者可能会利用目录遍历(directory traversal)或者等价路径(path equivalence)的方式来绕过这些限制。
错误示例:
public static void main(String[] args)
{
File f = new File(System.getProperty("user.home")
+ System.getProperty("file.separator") + args[0]);
String absPath = f.getAbsolutePath();
if (!isInSecureDir(Paths.get(absPath)))
{
// Refer to Rule 3.5 for the details of isInSecureDir()
throw new IllegalArgumentException();
}
if (!validate(absPath))
{
// Validation
throw new IllegalArgumentException();
}
/* … */
}
正确示例:
public static void main(String[] args) throws IOException
{
File f = new File(System.getProperty("user.home")
+ System.getProperty("file.separator") + args[0]);
String canonicalPath = f.getCanonicalPath();
if (!isInSecureDir(Paths.get(absPath)))
{
// Refer to Rule 3.5 for the details of isInSecureDir()
throw new IllegalArgumentException();
}
if (!validate(absPath))
{
// Validation
throw new IllegalArgumentException();
}
/* ... */
}
安全地从ZipInputStream提取文件
📌异常行为
不要 抑制或忽略 已检查到的异常
禁止在异常中泄露敏感信息
错误示例:
public class ExceptionExample{
public static void main(String[] args) throws FileNotFoundException{
// Linux stores a user's home directory path in
// the environment variable $HOME, Windows in %APPDATA%
// ... Other omitted code
FileInputStream fis = new FileInputStream(System.getenv("APPDATA") + args[0]);
}
}
错误示例:
try{
FileInputStream fis = new FileInputStream(System.getenv("APPDATA") + args[0]);
}catch (FileNotFoundException e){
// Log the exception
throw new IOException("Unable to retrieve file", e);
正确示例:
public class ExceptionExample{
public static void main(String[] args){
File file = null;
try{
file = new File(System.getenv("APPDATA") + args[0]).getCanonicalFile();
if (!file.getPath().startsWith("c:\\homepath")){
System.out.println("Invalid file");
return;
}
}catch (IOException x){
System.out.println("Invalid file");
return;
}
try{
FileInputStream fis = new FileInputStream(file);
}catch (FileNotFoundException x){
System.out.println("Invalid file");
return;
}
}
在这个正确示例中,规定用户只能打开c:\homepath目录下的文件,用户不可能发现这个目录以外的任何信息。在这个方案中,如果无法打开文件,或者文件不在合法的目录下,则会产生一条简洁的错误消息。任何c:\homepath目录以外的文件信息都被会隐蔽起来。
异常发生时要恢复到之前的对象状态
错误示例:
int a = 10;
try{
a +=5;//改变状态
validate(a);//基于新状态操作,可能抛出异常
a -=5;//恢复状态
}catch(Exception e){
//
}
上述示例当基于新状态操作抛出异常,那么对象状态将不会恢复到原始状态。
正确示例(回滚):
int a = 10;
try{
a +=5;//改变状态
validate(a);//基于新状态操作,可能抛出异常
a -=5;//恢复状态
}catch(Exception e){
//
a -=5;
}
正确示例(finally):
int a = 10;
try{
a +=5;//改变状态
validate(a);//基于新状态操作,可能抛出异常
}catch(Exception e){
//
}finally{
a -= 5;//恢复状态
}
正确示例(仅处理异常)
int a = 10;
a +=5;//改变状态
try{
validate(a);//基于新状态操作,可能抛出异常
}catch(Exception e){
//
}
a -= 5;//恢复状态
📌IO操作
临时文件使用完毕应该及时删除
错误示例:
正确示例(delete on close)
Path tempFile = null;
try{
tempFile = Files.createTempFile("tempnam", ".tmp");
try (BufferedWriter writer = Files.newBufferedWriter(tempFile,
Charset.forName("UTF8"),
StandardOpenOption.DELETE_ON_CLOSE))
{
// write to the file and use it
}
System.out.println("Temporary file write done, file erased");
}catch (IOException x){
// Some other sort of failure, such as permissions.
System.err.println("Error creating temporary file");
}
正确示例(手动删除):
File f = File.createTempFile("tempnam", ".tmp");
FileOutputStream fop = null;
try
{
fop = new FileOutputStream(f);
// write to the file and use it
}
finally
{
if (fop != null)
{
try
{
fop.close();
}
catch (IOException x)
{
// handle error
}
if (!f.delete())// delete file when finished
{
// log the error
}
}
避免让外部进程阻塞在输入输出流上
📌序列化与反序列化
敏感对象 离开信任区 需要进行签名并加密
敏感数据传输过程中要防止窃取和恶意篡改。使用安全的加密算法加密传输对象可以保护数据。
在以下场景中,需要对对象密封和数字签名来保证数据安全:
1) 序列化或传输敏感数据
2) 没有诸如SSL传输通道一类的安全通信通道或者对于有限的事务来说代价太高
3) 敏感数据需要长久保存(比如在硬盘驱动器上)
应避免使用私有加密算法,这类算法大多数情况下会引入不必要的漏洞,在readObject()和writeObject()函数中使用私有加密算法是典型的反面示例。
错误示例:
class SerializableMap<K, V> implements Serializable
{
final static long serialVersionUID = 45217497203262395L;
private Map<K, V> map;
public SerializableMap()
{
map = new HashMap<K, V>();
}
public V getData(K key)
{
return map.get(key);
}
public void setData(K key, V data)
{
map.put(key, data);
}
}
public class MapSerializer
{
public static SerializableMap<String, Integer> buildMap()
{
SerializableMap<String, Integer> map = new SerializableMap<String, Integer>();
map.setData("John Doe", new Integer(123456789));
map.setData("Richard Roe", new Integer(246813579));
return map;
}
public static void InspectMap(SerializableMap<String, Integer> map)
{
System.out.println("John Doe's number is " + map.getData("John Doe"));
System.out.println("Richard Roe's number is "
+ map.getData("Richard Roe"));
}
}
public static void main(String[] args) throws IOException,ClassNotFoundException{
// Build map
SerializableMap<String, Integer> map = buildMap();
// Serialize map
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("data"));
out.writeObject(map);
out.close();
// Deserialize map
ObjectInputStream in = new ObjectInputStream(new FileInputStream("data"));
map = (SerializableMap<String, Integer>) in.readObject();
in.close();
// Inspect map
InspectMap(map);
}
错误示例(仅加密):
public static void main(String[] args) throws IOException,GeneralSecurityException, ClassNotFoundException{
// Build map
SerializableMap<String, Integer> map = buildMap();
// Generate sealing key & seal map
KeyGenerator generator = KeyGenerator.getInstance("AES");
generator.init(new SecureRandom());
Key key = generator.generateKey();
Cipher cipher = Cipher.getInstance("AES");
cipher.init(Cipher.ENCRYPT_MODE, key);
SealedObject sealedMap = new SealedObject(map, cipher);
// Serialize map
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("data"));
out.writeObject(sealedMap);
out.close();
// Deserialize map
ObjectInputStream in = new ObjectInputStream(new FileInputStream("data"));
sealedMap = (SealedObject) in.readObject();
in.close();
// Unseal map
cipher = Cipher.getInstance("AES");
cipher.init(Cipher.DECRYPT_MODE, key);
map = (SerializableMap<String, Integer>) sealedMap.getObject(cipher);
// Inspect map
InspectMap(map);
}
错误示例(先加密后签名):
正确示例(签名后加密):
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SealedObject;
// Other import…
public static void main(String[] args) throws IOException, GeneralSecurityException, ClassNotFoundException
{
// Build map
SerializableMap<String, Integer> map = buildMap();
// Generate signing public/private key pair & sign map
KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
KeyPair kp = kpg.generateKeyPair();
Signature sig = Signature.getInstance("SHA256withRSA");
SignedObject signedMap = new SignedObject(map, kp.getPrivate(), sig);
// Generate sealing key & seal map
KeyGenerator generator = KeyGenerator.getInstance("AES");
generator.init(new SecureRandom());
Key key = generator.generateKey();
Cipher cipher = Cipher.getInstance("AES");
cipher.init(Cipher.ENCRYPT_MODE, key);
SealedObject sealedMap = new SealedObject(signedMap, cipher);
// Serialize map
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(
"data"));
out.writeObject(sealedMap);
out.close();
// Deserialize map
ObjectInputStream in = new ObjectInputStream(new FileInputStream("data"));
sealedMap = (SealedObject) in.readObject();
in.close();
// Unseal map cipher = Cipher.getInstance("AES");
cipher.init(Cipher.DECRYPT_MODE, key);
signedMap = (SignedObject) sealedMap.getObject(cipher);
// Verify signature and retrieve map
if (!signedMap.verify(kp.getPublic(), sig))
{
throw new GeneralSecurityException("Map failed verification");
}
map = (SerializableMap<String, Integer>) signedMap.getObject();
// Inspect map
InspectMap(map);
}
敏感数据需要加密后才能序列化
因为序列化没有提供一个机制来保证安全性。
错误示例:
public class GPSLocation implements Serializable{
private double x; // sensitive field
private double y; // sensitive field
private String id;// non-sensitive field
}
public class Coordinates{
public static void main(String[] args)
{
FileOutputStream fout = null;
try
{
GPSLocation p = new GPSLocation(5, 2, "northeast");
fout = new FileOutputStream("location.ser");
ObjectOutputStream oout = new ObjectOutputStream(fout);
oout.writeObject(p);
oout.close();
}
catch (Throwable t)
{
// Forward to handler
}
finally
{
if (fout != null)
{
try
{
fout.close();
}
catch (IOException x)
{
// handle error
}
}
}
}
}
正确示例(使用transient禁止序列化敏感数据):
public class GPSLocation implements Serializable
{
private transient double x; // transient field will not be serialized
private transient double y; // transient field will not be serialized
private String id;
// other content
}
正确示例(排除敏感字段):
public class GPSLocation implements Serializable
{
private double x;
private double y;
private String id;
private static final ObjectStreamField[] serialPersistentFields = {new ObjectStreamField("id", String.class)};
}
📌平台安全
使用安全管理器来保护敏感操作
当应用需要加载非信任代码时,必须安装安全感管理器,且敏感操作必须经过安全感管理器检查,从而防止他们被非信任代码调用,某些常见敏感的JavaAPI(访问本地文件、向外部主机开放套接字连接、创建类加载器等),除了安装一个安全管理器之外,必须自定义安全策略,并且在操作之前手动为其增加安全管理器检查。
错误示例:
public class SensitiveHash
{
private Hashtable<Integer, String> ht = new Hashtable<Integer, String>();
public void removeEntry(Object key)
{
ht.remove(key);
}
}
哈希表中包含敏感信息,但是removeEntry方法被设定为了public且non-final,将其暴露给了恶意调用者。
正确示例:
public class SensitiveHash
{
Hashtable<Integer, String> ht = new Hashtable<Integer, String>();
void removeEntry(Object key)
{
// "removeKeyPermission" is a custom target name for SecurityPermission
check("removeKeyPermission");
ht.remove(key);
}
private void check(String directive)
{
SecurityManager sm = System.getSecurityManager();
if (sm != null)
{
sm.checkSecurityAccess(directive);
}
}
}
该段示例使用安全管理器检查来防止HashTable实例中的条目被恶意删除,如果调用者缺少 java.security.SecurityPermission.removeKeyPermission SecurityException异常将被抛出。
禁止基于不信任的数据源做安全检查
基于不受信任数据源的安全感检查可以被攻击者所绕过,在使用非受信数据源时,必须确保被检查的输入和实际被处理的输入相同。如果检查的数据和输入的数据不一致时,会产生“time-of-check,time-of-use”漏洞。
对于不受信任的数据源正确的策略是在做安全感检查之前,可以对不受信任的对象或者参数做防御性拷贝,基于这份拷贝做安全检查,仍需注意的是这样的拷贝必须是深拷贝,使用浅拷贝或者使用攻击者提供的拷贝方法仍可带来危害。
禁止特权块非信任域泄露敏感信息
编写自定义类加载器时应调用超类的getPermission()函数
自定义类加载器时不要直接继承抽象的ClassLoader类。
错误示例:
public class MyClassLoader extends URLClassLoader
{
@Override
protected PermissionCollection getPermissions(CodeSource cs)
{
PermissionCollection pc = new Permissions();
pc.add(new RuntimePermission("exitVM"));
return pc;
}
// Other code…
}
错误代码继承了URLClassLoader类的自定义类加载器的一部分,它覆盖了getPermissions方法,但是并未调用其茶类的限制性更强的getPermissions方法。因此该自定义类加载器加载的类具有的权限,完全独立于系统全局策略文件规定的权限。
正确示例:
public class MyClassLoader extends URLClassLoader
{
@Override
protected PermissionCollection getPermissions(CodeSource cs)
{
PermissionCollection pc = super.getPermissions(cs);
pc.add(new RuntimePermission("exitVM"));
return pc;
}
// Other code…
}
📌运行环境
不要使用危险的许可与目标组合
有些许可和目标的组合会导致权限过大,而这些权限本不应该被赋予,另外有些权限只需要赋予特定的代码。
- 不要将AllPermission许可赋予给不信任的代码。
- ReflectPermission许可与suppressAccessChecks目标组合会抑制所有Java语言标准中的访问检查了,这个访问检查在一个类试图访问其他类的包私有,包保护,和私有成员的进行。因此,被授权的类能够访问任意其他类中任意的字段和方法。因此,不要将ReflectPermission许可和suppressAccessChecks目标组合使用。
- 如果将java.lang.RuntimePermission许可与createClassLoader目标组合,将赋予代码创建ClassLoader对象的权限。这将是非常危险的,因为恶意代码可以创建其自己特有的类加载器并通过类加载来为类分配任意许可。
不要禁止字节码验证
Java字节码验证其是JVM的一个内部组件,负责检测不合规的Java字节码,包括确保class文件的格式正确性、没有出现非法的类型转换不会出现调用栈下溢、确保每个方法最终都会将其调用栈中推入的东西删除。
用户通常觉得从可信的源获取的Java class文件是合规的,所以执行起来也是安全的,误以为字节码验证对于这些类来说是多余的。结果,用户可能会禁用字节码验证,破坏Java的安全性以及安全保障。字节码验证器一定不能被禁用。
错误示例:
java -Xverify:none ApplicationName
正确示例:
java ApplicationName
在生产环境中,必须禁用远程监控
Java提供了多种API让外部程序远程控制运行中的Java程序,这些API也允许不同主机上的程序远程监控Java程序,这些特性方便了对程序进行调试或者对其性能进行调优。
但是如果一个java程序被部署在生产环境中同时允许远程监控,攻击者很容易连接到JVM来监视这个Java程序的行为和数据。
错误示例(JVM Tool Interface):
${JDK_PATH}/bin/java -agentlib:libname=options ApplicationName
错误示例(JVM监控):
${JDK_PATH}/bin/java -Dcom.sun.management.jmxremote.port=8000 ApplicationName
正确示例:
${JDK_PATH}/bin/java -Djava.security.manager ApplicationName
将所有安全敏感代码都放在一个jar包中,签名再加密。
不要信任环境变量的值
如果确实需要用到环境变量,也需要在使用之前对其进行校验。
错误代码:
String username = System.getenv("USER");
正确示例:
String username = System.getProperty("user.name");
生产代码不应保留任何调试入口点
📌其他
禁止在日志中保存口令、密钥及其他敏感数据
对于敏感信息建议采取以下方法:
- 不在日志中打印敏感信息。
- 若因为特殊原因必须要打印日志,则用固定长度的星号(*)代替输出的敏感信息。
禁止使用私有或者弱加密算法
禁止使用私有算法或者弱加密算法(比如DES,SHA1等)。应该使用经过验证的、安全的、公开的加密算法。
加密算法分为对称加密算法和非对称加密算法。推荐使用的对称加密算法有:AES(128位)
推荐使用的非对称算法有:RSA(2048位)
推荐使用的数字签名算法有:DSA(2048位)、ECDSA
推荐使用的验证消息完整性算法有:安全哈希算法(SHA256)等基于哈希算法的口令安全存储必须加入盐值
禁止将敏感信息硬编码在程序中
错误代码:
public class IPaddress
{
private String ipAddress = "172.16.254.1";
public static void main(String[] args){
//...
}
}
恶意用户可以使用javap -c IPaddress命令来反编译class来发现其中硬编码的服务器IP地址。反编译器的输出信息透露了服务器的明文IP地址:172.16.254.1。
正确代码:
public class IPaddress
{
public static void main(String[] args) throws IOException
{
char[] ipAddress = new char[100];
BufferedReader br = new BufferedReader(new InputStreamReader(
new FileInputStream("serveripaddress.txt")));
// Reads the server IP address into the char array,
// returns the number of bytes read
int n = br.read(ipAddress);
// Validate server IP address
// Manually clear out the server IP address
// immediately after use
for (int i = n - 1; i >= 0; i--)
{
ipAddress[i] = 0;
}
br.close();
}
}
使用强随机数
- 为什么会设计伪随机数?
正确示例:
public byte[] genRandBytes(int len){
byte[] bytes = null;
if (len > 0 && len < 1024){
bytes = new byte[len];
SecureRandom random = new SecureRandom();
random.nextBytes(bytes);
}
return bytes;
}
使用SSLSocket代替Socket来进行安全数据交互
错误示例:
//Exception handling has been omitted for the sake of brevity
class EchoServer
{
public static void main(String[] args) throws IOException
{
//Exception handling has been omitted for the sake of brevity
//...
ServerSocket serverSocket = new ServerSocket(9999);
Socket socket = serverSocket.accept();
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
BufferedReader in = new BufferedReader(new InputStreamReader(
socket.getInputStream()));
String inputLine;
while ((inputLine = in.readLine()) != null)
{
System.out.println(inputLine);
out.println(inputLine);
}
// ...
}
// ...
}
class EchoClient
{
public static void main(String[] args) throws UnknownHostException,
IOException
{
// Exception handling has been omitted for the sake of brevity
// ...
Socket socket = new Socket(getServerIp(), 9999);
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
BufferedReader in = new BufferedReader(new InputStreamReader(
socket.getInputStream()));
BufferedReader stdIn = new BufferedReader(new InputStreamReader(
System.in));
String userInput;
while ((userInput = stdIn.readLine()) != null)
{
out.println(userInput);
System.out.println(in.readLine());
}
// ...
}
// ...
}
正确示例:
class EchoServer
{
public static void main(String[] args) throws IOException
{
// Exception handling has been omitted for the sake of brevity
// ...
SSLServerSocket SSLServerSocketFactory sslServerSocketFactory = (SSLServerSocketFactory) SSLServerSocketFactory.getDefault();
sslServerSocket = (SSLServerSocket) sslServerSocketFactory.createServerSocket(9999);
SSLSocket sslSocket = (SSLSocket) sslServerSocket.accept();
PrintWriter out = new PrintWriter(sslSocket.getOutputStream(), true);
BufferedReader in = new BufferedReader(new InputStreamReader(
sslSocket.getInputStream()));
String inputLine;
while ((inputLine = in.readLine()) != null)
{
System.out.println(inputLine);
out.println(inputLine);
}
// ...
}
// ...
}
class EchoClient
{
public static void main(String[] args) throws IOException
{
// Exception handling has been omitted for the sake of brevity
// ...
SSLSocket SSLSocketFactory sslSocketFactory = (SSLSocketFactory) SSLSocketFactory.getDefault();
sslSocket = (SSLSocket) sslSocketFactory.createSocket(getServerIp(),
9999);
PrintWriter out = new PrintWriter(sslSocket.getOutputStream(), true);
BufferedReader in = new BufferedReader(new InputStreamReader(
sslSocket.getInputStream()));
BufferedReader stdIn = new BufferedReader(new InputStreamReader(
System.in));
String userInput;
while ((userInput = stdIn.readLine()) != null)
{
out.println(userInput);
System.out.println(in.readLine());
}
// ...
}
// ...
}
