The HttpsPublisher
is simple enough in structure to illustrate the basics of wire-level security. Among the
several reasons for going with a production-grade web server such as Tomcat or Jetty is that these
servers provide such good support for HTTPS, at the application and at the administrative level.
Although these web servers provide first-rate HTTPS support,
they do require setup comparable to that illustrated with the HttpsPublisher
. This section focuses on Tomcat.
Tomcat does not ship with a keystore of digital certificates and, accordingly, does not enable HTTPS by default. The
service must be turned on by editing the configuration file TOMCAT_HOME/conf/server.xml, with details
provided shortly. The same keystore file used in the HttpsPublisher
example, test.keystore, could be
re-used for Tomcat. A modern browser connecting over HTTPS to Tomcat should complain that the
digital certificate in test.keystore is self-signed and, therefore, worthless as a security credential.
In production, a keystore with commercial-grade keystore digital certificates would be needed. Yet the point of immediate interest is
that Tomcat does require the programmer to jump through a few hoops in order to switch from an HTTP-accessible
to an HTTPS-accessible service. There are only three such hoops:
The first two changes are covered in Setting Up Tomcat for HTTPS Support.
A website or a web service can instruct Tomcat to enforce HTTPS access to either the entire resource (for instance,
all of the HTML pages in the website and all of the operations in the web service) or only parts thereof (for
instance, to administrative HTML pages in the site or to selected operations in the service). The instructions
to Tomcat occur in a security-constraint
section of the web.xml deployment file. To illustrate, the RESTful
predictions2 service of Chapter 2, originally deployed with HTTP access only, can be redeployed with
HTTPS access only. This requires no change whatsoever in the code.
The revised web.xml is Example 6-7.
Example 6-7. The web.xml revised for security
<?
xml
version
=
"1.0"
encoding
=
"UTF-8"
?>
<
web
-
app
>
<
servlet
>
<
servlet
-
name
>
predictor
</
servlet
-
name
>
<
servlet
-
class
>
predictions2
.
PredictionsServlet
</
servlet
-
class
>
</
servlet
>
<
security
-
constraint
>
![]()
<
web
-
resource
-
collection
>
<
url
-
pattern
>/*</
url
-
pattern
>
![]()
</
web
-
resource
-
collection
>
<
user
-
data
-
constraint
>
<
transport
-
guarantee
>
CONFIDENTIAL
</
transport
-
guarantee
>
![]()
</
user
-
data
-
constraint
>
</
security
-
constraint
>
<
servlet
-
mapping
>
<
servlet
-
name
>
predictor
</
servlet
-
name
>
<
url
-
pattern
>/*</
url
-
pattern
>
</
servlet
-
mapping
>
</
web
-
app
>
The changes are limited to the security-constraint
section (line 1). In this example,
the security constraint is enforced on the entire resource because the url-pattern
(line 2)
has /*
as its value. The deployed WAR file predictions2.war (created, as usual, with the
Ant script) could be partitioned in subdirectories, for example:
/admin ;; contains administrative operations /public ;; contains publicly accessible operations
Under this partition, the service operations in the /admin
directory might require HTTPS
but not the ones in the /public
directory. To enforce this policy, the url-pattern
in line 2
would change to /admin/*
. The transport-guarantee
element, with a value of
CONFIDENTIAL
(line 3), instructs Tomcat to enforce HTTPS access on the specified resource, in this
example on the entire predictions2 WAR file. If a client tried to access the
predictions2 service under HTTP, Tomcat would respond with an HTTP status code of 302 and the
appropriate https URL, thereby signaling to the client that a new request with an HTTPS connection should be
attempted.
Within the web-resource-collection
element of web.xml, access constraints can be specified
that depend on the HTTP verb of the client request. For example, the web.xml segment:
<
web
-
resource
-
collection
>
<
url
-
pattern
>/*</
url
-
pattern
>
<
http
-
method
>
POST
</
http
-
method
>
![]()
<
http
-
method
>
PUT
</
http
-
method
>
![]()
</
web
-
resource
-
collection
>
specifies that access to the resource, in this case the entire predictions2 service, is constrained only on POST and PUT requests (lines 1 and 2). If no specific HTTP verbs are specified, then the constraint covers them all.
Example 6-8 shows the HttpsPredictionsClient
against the predictions2 service.
Example 6-8. The HttpsPredictionsClient
against the predictions2 service
import
java.net.URL
;
import
javax.net.ssl.HttpsURLConnection
;
import
javax.net.ssl.SSLContext
;
import
java.security.KeyStore
;
import
javax.net.ssl.TrustManagerFactory
;
import
javax.net.ssl.X509TrustManager
;
import
javax.net.ssl.HostnameVerifier
;
import
javax.net.ssl.SSLSession
;
import
java.security.cert.X509Certificate
;
import
java.security.SecureRandom
;
import
java.io.FileInputStream
;
import
java.io.InputStream
;
import
java.io.OutputStream
;
import
java.io.ByteArrayOutputStream
;
public
class
PredictionsHttpsClient
{
private
static
final
String
endpoint
=
"https://localhost:8443/predictions2"
;
private
static
final
String
truststore
=
"test.keystore"
;
public
static
void
main
(
String
[
]
args
)
{
new
PredictionsHttpsClient
().
runTests
();
}
private
void
runTests
()
{
try
{
SSLContext
sslCtx
=
SSLContext
.
getInstance
(
"TLS"
);
char
[
]
password
=
"qubits"
.
toCharArray
();
KeyStore
ks
=
KeyStore
.
getInstance
(
"JKS"
);
FileInputStream
fis
=
new
FileInputStream
(
truststore
);
ks
.
load
(
fis
,
password
);
TrustManagerFactory
tmf
=
TrustManagerFactory
.
getInstance
(
"SunX509"
);
tmf
.
init
(
ks
);
// same as keystore
sslCtx
.
init
(
null
,
// not needed, not challenged
tmf
.
getTrustManagers
(),
new
SecureRandom
());
HttpsURLConnection
.
setDefaultSSLSocketFactory
(
sslCtx
.
getSocketFactory
());
getTest
();
postTest
();
getTestAll
();
// confirm POST test
deleteTest
(
"31"
);
getTestAll
();
// confirm DELETE test
}
catch
(
Exception
e
)
{
throw
new
RuntimeException
(
e
);
}
}
private
HttpsURLConnection
getConnection
(
URL
url
,
String
verb
)
{
try
{
HttpsURLConnection
conn
=
(
HttpsURLConnection
)
url
.
openConnection
();
conn
.
setDoInput
(
true
);
conn
.
setDoOutput
(
true
);
conn
.
setRequestMethod
(
verb
);
conn
.
setHostnameVerifier
(
new
HostnameVerifier
()
{
public
boolean
verify
(
String
host
,
SSLSession
session
)
{
return
host
.
equals
(
"localhost"
);
// for development
}
});
return
conn
;
}
catch
(
Exception
e
)
{
throw
new
RuntimeException
(
e
);
}
}
private
void
getTest
()
{
getTestAll
();
getTestOne
(
"31"
);
}
private
void
getTestAll
()
{
try
{
URL
url
=
new
URL
(
endpoint
);
HttpsURLConnection
conn
=
getConnection
(
url
,
"GET"
);
conn
.
connect
();
readResponse
(
"GET all request:\n"
,
conn
);
conn
.
disconnect
();
}
catch
(
Exception
e
)
{
throw
new
RuntimeException
(
e
);
}
}
private
void
getTestOne
(
String
id
)
{
try
{
URL
url
=
new
URL
(
endpoint
+
"?id="
+
id
);
HttpsURLConnection
conn
=
getConnection
(
url
,
"GET"
);
conn
.
connect
();
readResponse
(
"GET request for "
+
id
+
":\n"
,
conn
);
conn
.
disconnect
();
}
catch
(
Exception
e
)
{
throw
new
RuntimeException
(
e
);
}
}
private
void
postTest
()
{
try
{
URL
url
=
new
URL
(
endpoint
);
HttpsURLConnection
conn
=
getConnection
(
url
,
"POST"
);
conn
.
connect
();
writeBody
(
conn
);
readResponse
(
"POST request:\n"
,
conn
);
conn
.
disconnect
();
}
catch
(
Exception
e
)
{
throw
new
RuntimeException
(
e
);
}
}
private
void
deleteTest
(
String
id
)
{
try
{
URL
url
=
new
URL
(
endpoint
+
"?id="
+
id
);
HttpsURLConnection
conn
=
getConnection
(
url
,
"DELETE"
);
conn
.
connect
();
readResponse
(
"DELETE request:\n"
,
conn
);
conn
.
disconnect
();
}
catch
(
Exception
e
)
{
throw
new
RuntimeException
(
e
);
}
}
private
void
writeBody
(
HttpsURLConnection
conn
)
{
try
{
String
pairs
=
"who=Freddy&what=Avoid Friday nights if possible."
;
OutputStream
out
=
conn
.
getOutputStream
();
out
.
write
(
pairs
.
getBytes
());
out
.
flush
();
}
catch
(
Exception
e
)
{
throw
new
RuntimeException
(
e
);
}
}
private
void
readResponse
(
String
msg
,
HttpsURLConnection
conn
)
{
try
{
byte
[
]
buffer
=
new
byte
[
4096
];
InputStream
in
=
conn
.
getInputStream
();
ByteArrayOutputStream
out
=
new
ByteArrayOutputStream
();
int
n
=
0
;
while
((
n
=
in
.
read
(
buffer
))
!=
-
1
)
out
.
write
(
buffer
,
0
,
n
);
in
.
close
();
System
.
out
.
println
(
new
String
(
out
.
toByteArray
()));
// stringify and print
}
catch
(
Exception
e
)
{
throw
new
RuntimeException
(
e
);
}
}
}
The PredictionsHttpsClient
(see Example 6-8) is a test client against
the HTTPS-deployed version of the predictions2 service. This client is roughly similar to the
HttpsClient
(Example 6-6) but methods such as readResponse
and writeBody
now are
beefed up in order to make realistic CRUD requests against the service. For example, the
postTest
adds new Prediction
to the collection, which requires that writeBody
insert
the key/value pairs for the key who
(the predictor) and the key what
(the prediction); the
getTestAll
must read all of the bytes returned from the service in order to display the
Prediction
list.
Tomcat’s approach to HTTPS exemplifies the separation-of-concerns principle. A web service (or a website) need not be changed at the code level to move from HTTP to HTTPS access. It bears repeating that no code in the original predictions2 service had to be changed; instead, only the deployment descriptor web.xml needed to change, and then only a little. Tomcat also assumes responsibility for enforcing HTTPS access in accordance with the policy given in the web.xml document: a client that now tries to hit the predictions2 service with an HTTP-based request is signaled that an HTTPS-based request should be used instead.