Skip to content

Commit

Permalink
Merge pull request #4821 from cyrusimap/jmap_upload_problem_details
Browse files Browse the repository at this point in the history
http_jmap.c: create Problem Details Object on JMAP upload failure
  • Loading branch information
ksmurchison committed Mar 4, 2024
2 parents 47e8b0b + 146adea commit aa57916
Show file tree
Hide file tree
Showing 2 changed files with 91 additions and 8 deletions.
51 changes: 51 additions & 0 deletions cassandane/Cassandane/Cyrus/JMAPCore.pm
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ sub new
$config->set(caldav_realm => 'Cassandane',
conversations => 'yes',
httpmodules => 'carddav caldav jmap',
jmap_max_size_upload => '1k',
httpallowcompress => 'no');

return $class->SUPER::new({
Expand Down Expand Up @@ -998,4 +999,54 @@ sub test_get_session_rename_race
$self->assert_not_null($RawResponse->{headers}{'retry-after'});
}

sub test_blob_upload_too_large
:min_version_3_9 :needs_component_jmap :JMAPExtensions
{
my $self = shift;
my $jmap = $self->{jmap};

xlog "Assert Problem Details report";
my $httpReq = {
headers => {
'Authorization' => $jmap->auth_header(),
},
content => 'X' x 1025,
};
my $httpRes = $jmap->ua->post($jmap->uploaduri('cassandane'), $httpReq);
if ($ENV{DEBUGJMAP}) {
warn "JMAP " . Dumper($httpReq, $httpRes);
}
$self->assert_str_equals("413", $httpRes->{status});
my $res = eval { decode_json($httpRes->{content}) };
$self->assert_str_equals("413", $res->{status});
$self->assert_str_equals("Content Too Large", $res->{title});
$self->assert_str_equals("urn:ietf:params:jmap:error:limit", $res->{type});
$self->assert_str_equals("maxSizeUpload", $res->{limit});
}

sub test_blob_upload_bad_url
:min_version_3_9 :needs_component_jmap :JMAPExtensions
{
my $self = shift;
my $jmap = $self->{jmap};

xlog "Assert Problem Details report";
my $httpReq = {
headers => {
'Authorization' => $jmap->auth_header(),
},
content => 'Hello World',
};
my $httpRes = $jmap->ua->post($jmap->uploaduri('cassandane') . 'X', $httpReq);
if ($ENV{DEBUGJMAP}) {
warn "JMAP " . Dumper($httpReq, $httpRes);
}
$self->assert_str_equals("404", $httpRes->{status});
my $res = eval { decode_json($httpRes->{content}) };
$self->assert_str_equals("404", $res->{status});
$self->assert_str_equals("Not Found", $res->{title});
$self->assert_str_equals("about:blank", $res->{type});
$self->assert_str_equals("unknown uploadUrl", $res->{detail});
}

1;
48 changes: 40 additions & 8 deletions imap/http_jmap.c
Original file line number Diff line number Diff line change
Expand Up @@ -1022,6 +1022,7 @@ static int jmap_upload(struct transaction_t *txn)
const char **hdr;
time_t now = time(NULL);
struct appendstate as;
char *accountid = NULL;
char *normalisedtype = NULL;
int rawmessage = 0;
struct conversations_state *cstate = NULL;
Expand All @@ -1041,14 +1042,16 @@ static int jmap_upload(struct transaction_t *txn)

if (datalen > (size_t) my_jmap_settings.limits[MAX_SIZE_UPLOAD]) {
txn->error.desc = "JSON upload byte size exceeds maxSizeUpload";
return HTTP_CONTENT_TOO_LARGE;
ret = HTTP_CONTENT_TOO_LARGE;
goto done;
}

/* Resource must be {accountId}/ with no trailing path */
char *accountid = xstrdup(txn->req_tgt.resource);
accountid = xstrdup(txn->req_tgt.resource);
char *slash = strchr(accountid, '/');
if (!slash || *(slash + 1) != '\0') {
ret = HTTP_NOT_FOUND;
txn->error.desc = "unknown uploadUrl";
goto done;
}
*slash = '\0';
Expand All @@ -1058,6 +1061,7 @@ static int jmap_upload(struct transaction_t *txn)
syslog(LOG_ERR, "jmap_upload: can't open conversations db for %s: %s",
accountid, error_message(r));
ret = HTTP_SERVER_ERROR;
txn->error.desc = "can't open upload conversations db";
goto done;
}

Expand All @@ -1066,6 +1070,7 @@ static int jmap_upload(struct transaction_t *txn)
syslog(LOG_ERR, "jmap_upload: can't open upload collection for %s: %s",
accountid, error_message(r));
ret = HTTP_NOT_FOUND;
txn->error.desc = "can't open upload collection";
goto done;
}

Expand Down Expand Up @@ -1197,8 +1202,15 @@ static int jmap_upload(struct transaction_t *txn)
append_abort(&as);
syslog(LOG_ERR, "append_fromstage(%s) failed: %s",
mailbox_name(mailbox), error_message(r));
ret = HTTP_SERVER_ERROR;
txn->error.desc = "append_fromstage() failed";
if (r == IMAP_QUOTA_EXCEEDED || r == IMAP_NO_OVERQUOTA) {
/* XXX Should never happen, but DTRT anyways */
ret = HTTP_NO_STORAGE;
txn->error.desc = "Mailbox is over quota";
}
else {
ret = HTTP_SERVER_ERROR;
txn->error.desc = "append_fromstage() failed";
}
goto done;
}

Expand All @@ -1224,6 +1236,8 @@ static int jmap_upload(struct transaction_t *txn)
json_object_set_new(resp, "expires", json_string(datestr));
json_object_set_new(resp, "type", json_string(normalisedtype));

ret = HTTP_CREATED;

done:
free(normalisedtype);
free(accountid);
Expand All @@ -1250,11 +1264,29 @@ static int jmap_upload(struct transaction_t *txn)
// checkpoint before replying
sync_checkpoint(httpd_in);

/* Output the JSON object */
if (resp)
ret = json_response(HTTP_CREATED, txn, resp);
if (!resp) {
/* Create Problem Details Object */
const char *type, *limit;

return ret;
if (ret == HTTP_CONTENT_TOO_LARGE) {
type = "urn:ietf:params:jmap:error:limit";
limit = "maxSizeUpload";
}
else {
type = "about:blank";
limit = NULL;
}

resp = json_pack("{s:s s:s* s:s s:i s:s}",
"type", type,
"limit", limit,
"title", error_message(ret) + 4,
"status", atoi(error_message(ret)),
"detail", txn->error.desc);
}

/* Output the JSON object */
return json_response(ret, txn, resp);
}

/* Handle a GET on the session endpoint */
Expand Down

0 comments on commit aa57916

Please sign in to comment.