Add content type class for sending headers & matching responses

Note that android doesn't include javax so we can't use it.
This commit is contained in:
Jeremy Lakeman 2018-03-26 14:38:00 +10:30
parent 262df2804d
commit ad2f0364cf
14 changed files with 207 additions and 87 deletions

View File

@ -51,7 +51,7 @@ public abstract class AbstractId {
return binary;
}
public abstract String getMimeType();
public abstract ContentType getMimeType();
public AbstractId(String hex) throws InvalidHexException {
if (hex==null)

View File

@ -74,11 +74,16 @@ public abstract class AbstractJsonList<T, E extends Exception> {
httpConnection = httpConnector.newServalDHttpConnection(request.verb, request.url);
httpConnection.connect();
if ("application/json".equals(httpConnection.getContentType())){
try {
ContentType contentType = new ContentType(httpConnection.getContentType());
if (ContentType.applicationJson.matches(contentType)){
json = new JSONTokeniser(
(httpConnection.getResponseCode() >= 300) ?
httpConnection.getErrorStream() : httpConnection.getInputStream());
}
} catch (ContentType.ContentTypeException e) {
throw new ServalDInterfaceException("malformed HTTP Content-Type: " + httpConnection.getContentType(), e);
}
if (httpConnection.getResponseCode()!=200){
handleResponseError();

View File

@ -37,8 +37,9 @@ public class BundleId extends SigningKey {
super(binary);
}
private static ContentType mimeType = ContentType.fromConstant("rhizome/bid; format=hex");
@Override
public String getMimeType() {
return "rhizome/bid";
public ContentType getMimeType() {
return mimeType;
}
}

View File

@ -30,9 +30,10 @@ public class BundleKey extends AbstractId {
return 32;
}
private static ContentType mimeType = ContentType.fromConstant("rhizome/bundlekey; format=hex");
@Override
public String getMimeType() {
return "rhizome/bundlekey";
public ContentType getMimeType() {
return mimeType;
}
public BundleKey(String hex) throws InvalidHexException {

View File

@ -42,8 +42,9 @@ public class BundleSecret extends AbstractId {
super(binary);
}
private static ContentType mimeType = ContentType.fromConstant("rhizome/bundlesecret; format=hex");
@Override
public String getMimeType() {
return "rhizome/bundlesecret";
public ContentType getMimeType() {
return mimeType;
}
}

View File

@ -0,0 +1,122 @@
package org.servalproject.servaldna;
import java.util.HashMap;
import java.util.Map;
/**
* Android doesn't include javax.activation.MimeType, so we have to implement it ourselves
*/
public class ContentType {
public final String type;
public final String subType;
public final Map<String,String> parameters;
// a few common content types to match against
public static ContentType textPlain = fromConstant("text/plain; charset=utf-8");
public static ContentType applicationJson = fromConstant("application/json");
public static ContentType applicationOctetStream = fromConstant("application/octet-stream");
public class ContentTypeException extends Exception{
ContentTypeException(String message) {
super(message);
}
}
public ContentType(String contentType) throws ContentTypeException {
int delim = contentType.indexOf(';');
if (delim < 0)
delim = contentType.length();
int slash = contentType.indexOf('/');
if (slash<0 || slash >delim)
throw new ContentTypeException("Failed to parse "+contentType);
type = contentType.substring(0,slash).trim().toLowerCase();
subType = contentType.substring(slash+1,delim).trim().toLowerCase();
parameters = new HashMap<>();
while(delim < contentType.length()){
int eq = contentType.indexOf('=',delim);
String name = contentType.substring(delim+1,eq).trim().toLowerCase();
String value;
if (contentType.charAt(eq+1)=='"'){
delim = eq+1;
StringBuilder sb = new StringBuilder();
while(true){
if (delim >= contentType.length())
throw new ContentTypeException("Failed to parse "+contentType);
char c=contentType.charAt(delim++);
if (c == '"')
break;
if (c == '\\') {
if (delim >= contentType.length())
throw new ContentTypeException("Failed to parse "+contentType);
c = contentType.charAt(delim++);
}
sb.append(c);
}
value = sb.toString();
while(delim < contentType.length()){
char c=contentType.charAt(delim++);
if (c==';')
break;
if (c!=' ')
throw new ContentTypeException("Failed to parse "+contentType);
}
}else{
delim = contentType.indexOf(';',eq);
if (delim <0)
delim = contentType.length();
value = contentType.substring(eq+1,delim).trim();
}
parameters.put(name, value);
}
}
public boolean matches(ContentType other) {
if (other==null)
return false;
if (type.equals(other.type) &&
(subType.equals(other.subType) || subType.equals("*"))){
for(Map.Entry<String,String> e: parameters.entrySet()){
if (!e.getValue().equals(other.parameters.get(e.getKey())))
return false;
}
return true;
}
return false;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb
.append(type)
.append('/')
.append(subType);
if (!parameters.isEmpty()){
for(Map.Entry<String,String> e: parameters.entrySet()){
sb
.append(';')
.append(e.getKey())
.append('=');
String value = e.getValue();
if (value.indexOf(';')>0 || value.indexOf('"')>0){
sb
.append('"')
.append(value.replace("\"","\\\""))
.append('"');
}else{
sb.append(value);
}
}
}
return sb.toString();
}
public static ContentType fromConstant(String contentType){
try {
return new ContentType(contentType);
} catch (ContentTypeException e) {
throw new IllegalStateException(e);
}
}
}

View File

@ -32,9 +32,10 @@ public class FileHash extends AbstractId {
return BINARY_SIZE;
}
private static ContentType mimeType = ContentType.fromConstant("rhizome/filehash; format=hex");
@Override
public String getMimeType() {
return "rhizome/filehash";
public ContentType getMimeType() {
return mimeType;
}
public FileHash(String hex) throws InvalidHexException {

View File

@ -77,7 +77,7 @@ public class PostHelper {
sb.append('"');
}
protected void writeHeading(String name, String filename, String type, String encoding)
protected void writeHeading(String name, String filename, ContentType type, String encoding)
{
StringBuilder sb = new StringBuilder();
sb.append("\r\n--").append(boundary).append("\r\n");
@ -88,7 +88,7 @@ public class PostHelper {
quoteString(sb, filename);
}
sb.append("\r\n");
sb.append("Content-Type: ").append(type).append("\r\n");
sb.append("Content-Type: ").append(type.toString()).append("\r\n");
if (encoding!=null)
sb.append("Content-Transfer-Encoding: ").append(encoding).append("\r\n");
sb.append("\r\n");
@ -96,17 +96,17 @@ public class PostHelper {
}
public void writeField(String name, String value){
writeHeading(name, null, "text/plain; charset=utf-8", null);
writeHeading(name, null, ContentType.textPlain, null);
writer.print(value);
}
public void writeField(String name, AbstractId value){
writeHeading(name, null, value.getMimeType() + "; format=hex", null);
writeHeading(name, null, value.getMimeType(), null);
writer.print(value.toHex());
}
public OutputStream beginFileField(String name, String filename){
writeHeading(name, filename, "application/octet-stream", "binary");
writeHeading(name, filename, ContentType.applicationOctetStream, "binary");
writer.flush();
return output;
}
@ -119,13 +119,13 @@ public class PostHelper {
output.write(buffer, 0, n);
}
public void writeField(String name, String type, byte value[]) throws IOException {
public void writeField(String name, ContentType type, byte value[]) throws IOException {
writeHeading(name, null, type, "binary");
writer.flush();
output.write(value);
}
public void writeField(String name, String type, byte value[], int offset, int length) throws IOException {
public void writeField(String name, ContentType type, byte value[], int offset, int length) throws IOException {
writeHeading(name, null, type, "binary");
writer.flush();
output.write(value, offset, length);

View File

@ -42,9 +42,10 @@ public class SigningKey extends AbstractId {
return BINARY_SIZE;
}
private static ContentType mimeType = ContentType.fromConstant("serval/identity; format=hex");
@Override
public String getMimeType() {
return "serval/identity";
public ContentType getMimeType() {
return mimeType;
}
@Override

View File

@ -31,9 +31,10 @@ public class SubscriberId extends AbstractId {
return BINARY_SIZE;
}
private static ContentType mimeType = ContentType.fromConstant("serval/sid; format=hex");
@Override
public String getMimeType() {
return "serval/sid";
public ContentType getMimeType() {
return mimeType;
}
public SubscriberId(String hex) throws InvalidHexException {

View File

@ -23,6 +23,7 @@ package org.servalproject.servaldna.keyring;
import org.servalproject.json.JSONInputException;
import org.servalproject.json.JSONTokeniser;
import org.servalproject.servaldna.ContentType;
import org.servalproject.servaldna.ServalDHttpConnectionFactory;
import org.servalproject.servaldna.ServalDInterfaceException;
import org.servalproject.servaldna.ServalDNotImplementedException;
@ -43,6 +44,7 @@ public class KeyringCommon
{
public static class Status {
ContentType contentType;
InputStream input_stream;
JSONTokeniser json;
public int http_status_code;
@ -61,14 +63,20 @@ public class KeyringCommon
Status status = new Status();
status.http_status_code = conn.getResponseCode();
status.http_status_message = conn.getResponseMessage();
try {
status.contentType = new ContentType(conn.getContentType());
} catch (ContentType.ContentTypeException e) {
throw new ServalDInterfaceException("malformed HTTP Content-Type: " + conn.getContentType(),e);
}
for (int code: expected_response_codes) {
if (status.http_status_code == code) {
status.input_stream = conn.getInputStream();
return status;
}
}
if (!conn.getContentType().equals("application/json"))
throw new ServalDInterfaceException("unexpected HTTP Content-Type: " + conn.getContentType());
if (!ContentType.applicationJson.matches(status.contentType))
throw new ServalDInterfaceException("unexpected HTTP Content-Type: " + status.contentType);
if (status.http_status_code >= 300) {
status.json = new JSONTokeniser(conn.getErrorStream());
decodeRestfulStatus(status);
@ -97,8 +105,6 @@ public class KeyringCommon
protected static Status receiveRestfulResponse(HttpURLConnection conn, int[] expected_response_codes) throws IOException, ServalDInterfaceException
{
Status status = receiveResponse(conn, expected_response_codes);
if (!conn.getContentType().equals("application/json"))
throw new ServalDInterfaceException("unexpected HTTP Content-Type: " + conn.getContentType());
status.json = new JSONTokeniser(status.input_stream);
return status;
}

View File

@ -23,6 +23,7 @@ package org.servalproject.servaldna.meshms;
import org.servalproject.json.JSONInputException;
import org.servalproject.json.JSONTokeniser;
import org.servalproject.servaldna.ContentType;
import org.servalproject.servaldna.PostHelper;
import org.servalproject.servaldna.ServalDFailureException;
import org.servalproject.servaldna.ServalDHttpConnectionFactory;
@ -44,18 +45,28 @@ public class MeshMSCommon
protected static JSONTokeniser receiveRestfulResponse(HttpURLConnection conn, int[] expected_response_codes) throws IOException, ServalDInterfaceException, MeshMSException
{
if (!"application/json".equals(conn.getContentType()))
throw new ServalDInterfaceException("unexpected HTTP Content-Type: " + conn.getContentType());
JSONTokeniser json = null;
try {
ContentType contentType = new ContentType(conn.getContentType());
if (ContentType.applicationJson.matches(contentType)){
if (conn.getResponseCode()>=300)
json = new JSONTokeniser(conn.getErrorStream());
else
json = new JSONTokeniser(conn.getInputStream());
}
} catch (ContentType.ContentTypeException e) {
throw new ServalDInterfaceException("malformed HTTP Content-Type: " + conn.getContentType());
}
for (int code: expected_response_codes) {
if (conn.getResponseCode() == code) {
JSONTokeniser json = new JSONTokeniser(conn.getInputStream());
if (json == null)
throw new ServalDInterfaceException("unexpected HTTP Content-Type: " + conn.getContentType());
return json;
}
}
switch (conn.getResponseCode()) {
case HttpURLConnection.HTTP_NOT_FOUND:
case 419: // Authentication Timeout, for missing secret
JSONTokeniser json = new JSONTokeniser(conn.getErrorStream());
Status status = decodeRestfulStatus(json);
throwRestfulResponseExceptions(status, conn.getURL());
throw new ServalDInterfaceException("unexpected MeshMS status = " + status.meshms_status_code + ", \"" + status.meshms_status_message + "\"");

View File

@ -26,6 +26,7 @@ import org.servalproject.json.JSONTokeniser;
import org.servalproject.servaldna.BundleId;
import org.servalproject.servaldna.BundleKey;
import org.servalproject.servaldna.BundleSecret;
import org.servalproject.servaldna.ContentType;
import org.servalproject.servaldna.FileHash;
import org.servalproject.servaldna.PostHelper;
import org.servalproject.servaldna.ServalDFailureException;
@ -49,25 +50,12 @@ import java.net.HttpURLConnection;
import java.net.ProtocolException;
import java.net.URL;
import java.util.Enumeration;
import javax.activation.MimeType;
import javax.activation.MimeTypeParseException;
public class RhizomeCommon
{
public static final MimeType MIME_TYPE_RHIZOME_MANIFEST;
static {
try {
MIME_TYPE_RHIZOME_MANIFEST = new MimeType("rhizome/manifest; format=text+binarysig");
}
catch (MimeTypeParseException ex) {
throw new IllegalStateException(ex);
}
}
private static class Status {
URL url;
String contentType;
ContentType contentType;
InputStream input_stream;
public int http_status_code;
public String http_status_message;
@ -77,17 +65,6 @@ public class RhizomeCommon
String payload_status_message;
}
protected static boolean mimeTypeMatches(MimeType required, MimeType candidate) {
if (!candidate.match(required))
return false;
for (Enumeration e = required.getParameters().getNames(); e.hasMoreElements(); ) {
String name = (String) e.nextElement();
if (!required.getParameter(name).equals(candidate.getParameter(name)))
return false;
}
return true;
}
protected static Status receiveResponse(HttpURLConnection conn, int expected_response_code) throws IOException, ServalDInterfaceException
{
int[] expected_response_codes = { expected_response_code };
@ -98,7 +75,11 @@ public class RhizomeCommon
{
Status status = new Status();
status.url = conn.getURL();
status.contentType = conn.getContentType();
try {
status.contentType = new ContentType(conn.getContentType());
} catch (ContentType.ContentTypeException e) {
throw new ServalDInterfaceException("malformed HTTP Content-Type: " + conn.getContentType(), e);
}
status.http_status_code = conn.getResponseCode();
status.http_status_message = conn.getResponseMessage();
for (int code: expected_response_codes) {
@ -107,8 +88,8 @@ public class RhizomeCommon
return status;
}
}
if (!status.contentType.equals("application/json"))
throw new ServalDInterfaceException("unexpected HTTP Content-Type: " + conn.getContentType());
if (!ContentType.applicationJson.matches(status.contentType))
throw new ServalDInterfaceException("unexpected HTTP Content-Type: " + status.contentType);
if (status.http_status_code >= 300) {
JSONTokeniser json = new JSONTokeniser(conn.getErrorStream());
@ -149,8 +130,8 @@ public class RhizomeCommon
protected static JSONTokeniser receiveRestfulResponse(HttpURLConnection conn, int[] expected_response_codes) throws IOException, ServalDInterfaceException
{
Status status = receiveResponse(conn, expected_response_codes);
if (!conn.getContentType().equals("application/json"))
throw new ServalDInterfaceException("unexpected HTTP Content-Type: " + conn.getContentType());
if (!ContentType.applicationJson.matches(status.contentType))
throw new ServalDInterfaceException("unexpected HTTP Content-Type: " + status.contentType);
if (status.input_stream == null)
throw new ServalDInterfaceException("unexpected HTTP response: " + status.http_status_code + " " + status.http_status_message);
return new JSONTokeniser(status.input_stream);
@ -255,26 +236,18 @@ public class RhizomeCommon
case NEW:
return null;
case SAME:
try {
MimeType content_type = new MimeType(conn.getContentType());
if (mimeTypeMatches(MIME_TYPE_RHIZOME_MANIFEST, content_type)) {
if (!RhizomeManifest.MIME_TYPE.matches(status.contentType))
throw new ServalDInterfaceException("unexpected HTTP Content-Type: " + status.contentType);
RhizomeManifest manifest = RhizomeManifest.fromTextFormat(status.input_stream);
BundleExtra extra = bundleExtraFromHeaders(conn);
return new RhizomeManifestBundle(manifest, extra.rowId, extra.insertTime, extra.author, extra.secret);
}
throw new ServalDInterfaceException("unexpected HTTP Content-Type: " + content_type);
}
catch (MimeTypeParseException ex) {
throw new ServalDInterfaceException("invalid HTTP Content-Type: " + conn.getContentType());
}
case ERROR:
throw new ServalDFailureException("received rhizome_bundle_status_code=ERROR(-1) from " + conn.getURL());
}
}
catch (RhizomeManifestParseException e) {
throw new ServalDInterfaceException("malformed manifest from daemon", e);
}
finally {
} finally {
if (status.input_stream != null)
status.input_stream.close();
}
@ -310,8 +283,8 @@ public class RhizomeCommon
}
// FALL THROUGH
case STORED: {
if (status.input_stream != null && !conn.getContentType().equals("application/octet-stream"))
throw new ServalDInterfaceException("unexpected HTTP Content-Type: " + conn.getContentType());
if (status.input_stream != null && !ContentType.applicationOctetStream.matches(status.contentType))
throw new ServalDInterfaceException("unexpected HTTP Content-Type: " + status.contentType);
RhizomeManifest manifest = manifestFromHeaders(conn);
BundleExtra extra = bundleExtraFromHeaders(conn);
RhizomePayloadRawBundle ret = new RhizomePayloadRawBundle(manifest, status.input_stream, extra.rowId, extra.insertTime, extra.author, extra.secret);
@ -386,8 +359,8 @@ public class RhizomeCommon
}
// FALL THROUGH
case STORED: {
if (status.input_stream != null && !conn.getContentType().equals("application/octet-stream"))
throw new ServalDInterfaceException("unexpected HTTP Content-Type: " + conn.getContentType());
if (status.input_stream != null && !ContentType.applicationOctetStream.matches(status.contentType))
throw new ServalDInterfaceException("unexpected HTTP Content-Type: " + status.contentType);
RhizomeManifest manifest = manifestFromHeaders(conn);
BundleExtra extra = bundleExtraFromHeaders(conn);
RhizomePayloadBundle ret = new RhizomePayloadBundle(manifest, status.input_stream, extra.rowId, extra.insertTime, extra.author, extra.secret);
@ -484,9 +457,8 @@ public class RhizomeCommon
decodeHeaderBundleStatus(status, conn);
checkBundleStatus(status);
MimeType content_type = new MimeType(status.contentType);
if (!mimeTypeMatches(MIME_TYPE_RHIZOME_MANIFEST, content_type))
throw new ServalDInterfaceException("unexpected HTTP Content-Type " + content_type + " from " + status.url + ", expecting " + MIME_TYPE_RHIZOME_MANIFEST);
if (!RhizomeManifest.MIME_TYPE.matches(status.contentType))
throw new ServalDInterfaceException("unexpected HTTP Content-Type " + status.contentType + " from " + status.url);
RhizomeManifest returned_manifest = RhizomeManifest.fromTextFormat(status.input_stream);
BundleExtra extra = bundleExtraFromHeaders(conn);
@ -495,9 +467,6 @@ public class RhizomeCommon
catch (RhizomeManifestParseException e) {
throw new ServalDInterfaceException("malformed manifest from daemon", e);
}
catch (MimeTypeParseException ex) {
throw new ServalDInterfaceException("invalid HTTP Content-Type: " + status.contentType);
}
finally {
if (status.input_stream != null)
status.input_stream.close();

View File

@ -35,6 +35,7 @@ import java.io.OutputStreamWriter;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import org.servalproject.servaldna.AbstractId;
import org.servalproject.servaldna.ContentType;
import org.servalproject.servaldna.SubscriberId;
import org.servalproject.servaldna.BundleId;
import org.servalproject.servaldna.FileHash;
@ -43,7 +44,7 @@ import org.servalproject.servaldna.BundleKey;
public class RhizomeManifest {
public final static int TEXT_FORMAT_MAX_SIZE = 8192;
public static final String MIME_TYPE = "rhizome/manifest; format=text+binarysig";
public static final ContentType MIME_TYPE = ContentType.fromConstant("rhizome/manifest; format=text+binarysig");
// Core fields used for routing and expiry (cannot be null)
public final BundleId id;