Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F117750432
http_jmap.c
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
Size
46 KB
Referenced Files
None
Subscribers
None
http_jmap.c
View Options
/* http_jmap.c -- Routines for handling JMAP requests in httpd
*
* Copyright (c) 1994-2018 Carnegie Mellon University. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in
* the documentation and/or other materials provided with the
* distribution.
*
* 3. The name "Carnegie Mellon University" must not be used to
* endorse or promote products derived from this software without
* prior written permission. For permission or any legal
* details, please contact
* Carnegie Mellon University
* Center for Technology Transfer and Enterprise Creation
* 4615 Forbes Avenue
* Suite 302
* Pittsburgh, PA 15213
* (412) 268-7393, fax: (412) 268-7395
* innovation@andrew.cmu.edu
*
* 4. Redistributions of any form whatsoever must retain the following
* acknowledgment:
* "This product includes software developed by Computing Services
* at Carnegie Mellon University (http://www.cmu.edu/computing/)."
*
* CARNEGIE MELLON UNIVERSITY DISCLAIMS ALL WARRANTIES WITH REGARD TO
* THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS, IN NO EVENT SHALL CARNEGIE MELLON UNIVERSITY BE LIABLE
* FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
* AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
* OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*
*/
#include
<config.h>
#include
<errno.h>
#include
"acl.h"
#include
"append.h"
#include
"httpd.h"
#include
"http_jmap.h"
#include
"http_proxy.h"
#include
"http_ws.h"
#include
"jmap_push.h"
#include
"mboxname.h"
#include
"proxy.h"
#include
"times.h"
#include
"sync_support.h"
#include
"syslog.h"
#include
"user.h"
#include
"xstrlcpy.h"
/* generated headers are not necessarily in current directory */
#include
"imap/http_err.h"
#include
"imap/imap_err.h"
#include
"imap/jmap_err.h"
#define JMAP_ROOT "/jmap"
#define JMAP_BASE_URL JMAP_ROOT "/"
#define JMAP_WS_COL "ws/"
#define JMAP_UPLOAD_COL "upload/"
#define JMAP_UPLOAD_TPL "{accountId}/"
#define JMAP_DOWNLOAD_COL "download/"
#define JMAP_DOWNLOAD_TPL "{accountId}/{blobId}/{name}?accept={type}"
#define JMAP_EVENTSOURCE_COL "eventsource/"
#define JMAP_EVENTSOURCE_TPL "?types={types}&closeafter={closeafter}&ping={ping}"
struct
namespace
jmap_namespace
;
static
time_t
compile_time
;
/* Namespace callbacks */
static
void
jmap_init
(
struct
buf
*
serverinfo
);
static
int
jmap_need_auth
(
struct
transaction_t
*
txn
);
static
int
jmap_auth
(
const
char
*
userid
);
static
void
jmap_reset
(
void
);
static
void
jmap_shutdown
(
void
);
static
int
jmap_parse_path
(
const
char
*
path
,
struct
request_target_t
*
tgt
,
const
char
**
resultstr
);
/* HTTP method handlers */
static
int
meth_get
(
struct
transaction_t
*
txn
,
void
*
params
);
static
int
meth_post
(
struct
transaction_t
*
txn
,
void
*
params
);
/* JMAP Requests */
static
int
jmap_get_session
(
struct
transaction_t
*
txn
);
static
int
jmap_download
(
struct
transaction_t
*
txn
);
static
int
jmap_upload
(
struct
transaction_t
*
txn
);
static
int
jmap_eventsource
(
struct
transaction_t
*
txn
);
/* WebSocket handler */
#define JMAP_WS_PROTOCOL "jmap"
static
ws_data_callback
jmap_ws
;
static
struct
connect_params
ws_params
=
{
&
jmap_parse_path
,
{
JMAP_BASE_URL
JMAP_WS_COL
,
JMAP_WS_PROTOCOL
,
&
jmap_ws
}
};
/* Namespace for JMAP */
struct
namespace_t
namespace_jmap
=
{
URL_NS_JMAP
,
0
,
"jmap"
,
JMAP_ROOT
,
"/.well-known/jmap"
,
jmap_need_auth
,
/*authschemes*/
0
,
/*mbtype*/
0
,
(
ALLOW_READ
|
ALLOW_POST
|
ALLOW_READONLY
),
&
jmap_init
,
&
jmap_auth
,
&
jmap_reset
,
&
jmap_shutdown
,
NULL
,
/*bearer*/
NULL
,
{
{
NULL
,
NULL
},
/* ACL */
{
NULL
,
NULL
},
/* BIND */
{
&
meth_connect
,
&
ws_params
},
/* CONNECT */
{
NULL
,
NULL
},
/* COPY */
{
NULL
,
NULL
},
/* DELETE */
{
&
meth_get
,
NULL
},
/* GET */
{
&
meth_get
,
NULL
},
/* HEAD */
{
NULL
,
NULL
},
/* LOCK */
{
NULL
,
NULL
},
/* MKCALENDAR */
{
NULL
,
NULL
},
/* MKCOL */
{
NULL
,
NULL
},
/* MOVE */
{
&
meth_options
,
&
jmap_parse_path
},
/* OPTIONS */
{
NULL
,
NULL
},
/* PATCH */
{
&
meth_post
,
NULL
},
/* POST */
{
NULL
,
NULL
},
/* PROPFIND */
{
NULL
,
NULL
},
/* PROPPATCH */
{
NULL
,
NULL
},
/* PUT */
{
NULL
,
NULL
},
/* REPORT */
{
&
meth_trace
,
NULL
},
/* TRACE */
{
NULL
,
NULL
},
/* UNBIND */
{
NULL
,
NULL
}
/* UNLOCK */
}
};
/*
* Namespace callbacks
*/
static
jmap_settings_t
my_jmap_settings
=
{
HASH_TABLE_INITIALIZER
,
NULL
,
{
0
},
PTRARRAY_INITIALIZER
,
PTRARRAY_INITIALIZER
};
static
void
jmap_init
(
struct
buf
*
serverinfo
)
{
#ifdef USE_XAPIAN
#include
"xapian_wrap.h"
buf_printf
(
serverinfo
,
" Xapian/%s"
,
xapian_version_string
());
#endif
namespace_jmap
.
enabled
=
config_httpmodules
&
IMAP_ENUM_HTTPMODULES_JMAP
;
if
(
namespace_jmap
.
enabled
&&
!
config_getswitch
(
IMAPOPT_CONVERSATIONS
))
{
syslog
(
LOG_ERR
,
"ERROR: cannot enable %s module with conversations disabled"
,
namespace_jmap
.
name
);
namespace_jmap
.
enabled
=
0
;
}
if
(
!
namespace_jmap
.
enabled
)
return
;
compile_time
=
calc_compile_time
(
__TIME__
,
__DATE__
);
initialize_JMAP_error_table
();
construct_hash_table
(
&
my_jmap_settings
.
methods
,
128
,
0
);
my_jmap_settings
.
server_capabilities
=
json_object
();
jmap_core_init
(
&
my_jmap_settings
);
jmap_mail_init
(
&
my_jmap_settings
);
jmap_mdn_init
(
&
my_jmap_settings
);
jmap_contact_init
(
&
my_jmap_settings
);
jmap_calendar_init
(
&
my_jmap_settings
);
jmap_backup_init
(
&
my_jmap_settings
);
jmap_notes_init
(
&
my_jmap_settings
);
#ifdef USE_SIEVE
jmap_sieve_init
(
&
my_jmap_settings
);
#endif
if
(
ws_enabled
)
{
jmap_push_poll
=
config_getduration
(
IMAPOPT_JMAP_PUSHPOLL
,
's'
);
if
(
jmap_push_poll
<
0
)
jmap_push_poll
=
0
;
json_object_set_new
(
my_jmap_settings
.
server_capabilities
,
JMAP_URN_WEBSOCKET
,
json_pack
(
"{s:s s:b}"
,
"url"
,
"wss:"
JMAP_BASE_URL
JMAP_WS_COL
,
"supportsPush"
,
jmap_push_poll
));
}
}
static
int
jmap_auth
(
const
char
*
userid
__attribute__
((
unused
)))
{
/* Set namespace */
mboxname_init_namespace
(
&
jmap_namespace
,
httpd_userisadmin
||
httpd_userisproxyadmin
);
return
0
;
}
static
int
jmap_need_auth
(
struct
transaction_t
*
txn
__attribute__
((
unused
)))
{
/* All endpoints require authentication */
return
HTTP_UNAUTHORIZED
;
}
static
void
jmap_reset
(
void
)
{
int
i
;
for
(
i
=
0
;
i
<
ptrarray_size
(
&
my_jmap_settings
.
event_handlers
);
i
++
)
{
struct
jmap_handler
*
h
=
ptrarray_nth
(
&
my_jmap_settings
.
event_handlers
,
i
);
if
(
h
->
eventmask
&
JMAP_HANDLE_CLOSE_CONN
)
{
h
->
handler
(
JMAP_HANDLE_CLOSE_CONN
,
NULL
,
h
->
rock
);
}
}
}
static
void
jmap_shutdown
(
void
)
{
free_hash_table
(
&
my_jmap_settings
.
methods
,
NULL
);
json_decref
(
my_jmap_settings
.
server_capabilities
);
ptrarray_fini
(
&
my_jmap_settings
.
getblob_handlers
);
int
i
;
for
(
i
=
0
;
i
<
ptrarray_size
(
&
my_jmap_settings
.
event_handlers
);
i
++
)
{
struct
jmap_handler
*
h
=
ptrarray_nth
(
&
my_jmap_settings
.
event_handlers
,
i
);
if
(
h
->
eventmask
&
JMAP_HANDLE_SHUTDOWN
)
{
h
->
handler
(
JMAP_HANDLE_SHUTDOWN
,
NULL
,
h
->
rock
);
}
free
(
h
);
}
ptrarray_fini
(
&
my_jmap_settings
.
event_handlers
);
}
/*
* HTTP method handlers
*/
enum
{
JMAP_ENDPOINT_API
,
JMAP_ENDPOINT_WS
,
JMAP_ENDPOINT_UPLOAD
,
JMAP_ENDPOINT_DOWNLOAD
,
JMAP_ENDPOINT_EVENTSOURCE
};
static
int
jmap_parse_path
(
const
char
*
path
,
struct
request_target_t
*
tgt
,
const
char
**
resultstr
)
{
size_t
len
;
char
*
p
;
if
(
*
tgt
->
path
)
return
0
;
/* Already parsed */
/* Make a working copy of target path */
strlcpy
(
tgt
->
path
,
path
,
sizeof
(
tgt
->
path
));
p
=
tgt
->
path
;
/* Sanity check namespace */
len
=
strlen
(
namespace_jmap
.
prefix
);
if
(
strlen
(
p
)
<
len
||
strncmp
(
namespace_jmap
.
prefix
,
p
,
len
)
||
(
path
[
len
]
&&
path
[
len
]
!=
'/'
))
{
*
resultstr
=
"Namespace mismatch request target path"
;
return
HTTP_FORBIDDEN
;
}
/* Always allow read, even if no content */
tgt
->
allow
=
ALLOW_READ
;
/* Skip namespace */
p
+=
len
;
/* Check for path after prefix */
if
(
*
p
==
'/'
)
p
++
;
if
(
*
p
)
{
/* Get "collection" */
tgt
->
collection
=
p
;
if
(
!
strncmp
(
tgt
->
collection
,
JMAP_UPLOAD_COL
,
strlen
(
JMAP_UPLOAD_COL
)))
{
tgt
->
flags
=
JMAP_ENDPOINT_UPLOAD
;
tgt
->
allow
|=
ALLOW_POST
;
/* Get "resource" which must be the accountId */
tgt
->
resource
=
tgt
->
collection
+
strlen
(
JMAP_UPLOAD_COL
);
}
else
if
(
!
strncmp
(
tgt
->
collection
,
JMAP_DOWNLOAD_COL
,
strlen
(
JMAP_DOWNLOAD_COL
)))
{
tgt
->
flags
=
JMAP_ENDPOINT_DOWNLOAD
;
/* Get "resource" */
tgt
->
resource
=
tgt
->
collection
+
strlen
(
JMAP_DOWNLOAD_COL
);
}
else
if
(
ws_enabled
&&
!
strcmp
(
tgt
->
collection
,
JMAP_WS_COL
))
{
tgt
->
flags
=
JMAP_ENDPOINT_WS
;
tgt
->
allow
|=
ALLOW_CONNECT
;
}
else
if
(
!
strncmp
(
tgt
->
collection
,
JMAP_EVENTSOURCE_COL
,
strlen
(
JMAP_EVENTSOURCE_COL
)))
{
tgt
->
flags
=
JMAP_ENDPOINT_EVENTSOURCE
;
}
else
{
return
HTTP_NOT_FOUND
;
}
}
else
{
tgt
->
flags
=
JMAP_ENDPOINT_API
;
tgt
->
allow
|=
ALLOW_POST
;
}
return
0
;
}
/* Perform a GET/HEAD request */
static
int
meth_get
(
struct
transaction_t
*
txn
,
void
*
params
__attribute__
((
unused
)))
{
int
r
=
jmap_parse_path
(
txn
->
req_uri
->
path
,
&
txn
->
req_tgt
,
&
txn
->
error
.
desc
);
if
(
r
)
return
r
;
if
(
!
(
txn
->
req_tgt
.
allow
&
ALLOW_READ
))
{
return
HTTP_NOT_FOUND
;
}
if
(
txn
->
req_tgt
.
flags
==
JMAP_ENDPOINT_API
)
{
return
jmap_get_session
(
txn
);
}
else
if
(
txn
->
req_tgt
.
flags
==
JMAP_ENDPOINT_DOWNLOAD
)
{
return
jmap_download
(
txn
);
}
/* Upgrade to WebSockets over HTTP/1.1 on WS endpoint, if requested */
else
if
((
txn
->
req_tgt
.
flags
==
JMAP_ENDPOINT_WS
)
&&
(
txn
->
flags
.
upgrade
&
UPGRADE_WS
))
{
return
ws_start_channel
(
txn
,
JMAP_WS_PROTOCOL
,
&
jmap_ws
);
}
else
if
(
txn
->
req_tgt
.
flags
==
JMAP_ENDPOINT_EVENTSOURCE
)
{
return
jmap_eventsource
(
txn
);
}
return
HTTP_NO_CONTENT
;
}
static
int
parse_json_body
(
struct
transaction_t
*
txn
,
json_t
**
req
)
{
json_error_t
jerr
;
/* Parse the JSON request */
*
req
=
json_loadb
(
buf_base
(
&
txn
->
req_body
.
payload
),
buf_len
(
&
txn
->
req_body
.
payload
),
0
,
&
jerr
);
if
(
!*
req
)
{
buf_reset
(
&
txn
->
buf
);
buf_printf
(
&
txn
->
buf
,
"Unable to parse JSON request body: %s"
,
jerr
.
text
);
txn
->
error
.
desc
=
buf_cstring
(
&
txn
->
buf
);
return
JMAP_NOT_JSON
;
}
return
0
;
}
static
int
json_response
(
int
code
,
struct
transaction_t
*
txn
,
json_t
*
root
)
{
size_t
flags
=
JSON_PRESERVE_ORDER
;
char
*
buf
;
/* Dump JSON object into a text buffer */
flags
|=
(
config_httpprettytelemetry
?
JSON_INDENT
(
2
)
:
JSON_COMPACT
);
buf
=
json_dumps
(
root
,
flags
);
json_decref
(
root
);
if
(
!
buf
)
{
txn
->
error
.
desc
=
"Error dumping JSON object"
;
return
HTTP_SERVER_ERROR
;
}
/* Output the JSON object */
switch
(
code
)
{
case
0
:
/* API request over WebSocket */
buf_initm
(
&
txn
->
resp_body
.
payload
,
buf
,
strlen
(
buf
));
return
0
;
case
HTTP_OK
:
case
HTTP_CREATED
:
txn
->
resp_body
.
type
=
"application/json; charset=utf-8"
;
break
;
default
:
txn
->
resp_body
.
type
=
"application/problem+json; charset=utf-8"
;
break
;
}
write_body
(
code
,
txn
,
buf
,
strlen
(
buf
));
free
(
buf
);
return
0
;
}
/* Perform a POST request */
static
int
meth_post
(
struct
transaction_t
*
txn
,
void
*
params
__attribute__
((
unused
)))
{
int
ret
;
json_t
*
req
=
NULL
,
*
res
=
NULL
;
ret
=
jmap_parse_path
(
txn
->
req_uri
->
path
,
&
txn
->
req_tgt
,
&
txn
->
error
.
desc
);
if
(
ret
)
return
ret
;
if
(
!
(
txn
->
req_tgt
.
allow
&
ALLOW_POST
))
{
return
HTTP_NOT_ALLOWED
;
}
/* Handle uploads */
if
(
txn
->
req_tgt
.
flags
==
JMAP_ENDPOINT_UPLOAD
)
{
return
jmap_upload
(
txn
);
}
/* Regular JMAP API request */
txn
->
req_body
.
flags
|=
BODY_DECODE
;
/* Check Content-Type */
const
char
**
hdr
=
spool_getheader
(
txn
->
req_hdrs
,
"Content-Type"
);
if
(
!
hdr
||
!
is_mediatype
(
"application/json"
,
hdr
[
0
]))
{
txn
->
error
.
desc
=
"This method requires a JSON request body"
;
ret
=
HTTP_BAD_MEDIATYPE
;
}
/* Read body */
else
if
((
ret
=
http_read_req_body
(
txn
)))
{
txn
->
flags
.
conn
=
CONN_CLOSE
;
}
/* Parse the JSON request */
else
if
(
!
(
ret
=
parse_json_body
(
txn
,
&
req
)))
{
ret
=
jmap_api
(
txn
,
req
,
&
res
,
&
my_jmap_settings
);
json_decref
(
req
);
}
if
(
ret
)
ret
=
jmap_error_response
(
txn
,
ret
,
&
res
);
/* ensure we didn't leak anything! */
assert
(
!
open_mailboxes_exist
());
assert
(
!
open_mboxlocks_exist
());
// checkpoint before we reply
sync_checkpoint
(
httpd_in
);
if
(
res
)
{
/* Output the JSON object */
ret
=
json_response
(
ret
?
ret
:
HTTP_OK
,
txn
,
res
);
}
syslog
(
LOG_DEBUG
,
">>>> jmap_post: Exit"
);
return
ret
;
}
/*
* JMAP Requests
*/
static
char
*
parse_accept_header
(
const
char
**
hdr
)
{
char
*
val
=
NULL
;
struct
accept
*
accept
=
parse_accept
(
hdr
);
if
(
accept
)
{
char
*
type
=
NULL
;
char
*
subtype
=
NULL
;
struct
param
*
params
=
NULL
;
message_parse_type
(
accept
->
token
,
&
type
,
&
subtype
,
&
params
);
if
(
type
&&
subtype
&&
!
strchr
(
type
,
'*'
)
&&
!
strchr
(
subtype
,
'*'
))
val
=
xstrdup
(
accept
->
token
);
free
(
type
);
free
(
subtype
);
param_free
(
&
params
);
struct
accept
*
tmp
;
for
(
tmp
=
accept
;
tmp
&&
tmp
->
token
;
tmp
++
)
{
free
(
tmp
->
token
);
}
free
(
accept
);
}
return
val
;
}
static
int
jmap_getblob_default_handler
(
jmap_req_t
*
req
,
jmap_getblob_context_t
*
ctx
)
{
struct
mailbox
*
mbox
=
NULL
;
msgrecord_t
*
mr
=
NULL
;
struct
body
*
body
=
NULL
;
const
struct
body
*
part
=
NULL
;
int
res
=
HTTP_OK
;
/* Find part containing blob */
int
r
=
jmap_findblob
(
req
,
ctx
->
from_accountid
,
ctx
->
blobid
,
&
mbox
,
&
mr
,
&
body
,
&
part
,
&
ctx
->
blob
);
if
(
r
)
{
res
=
HTTP_NOT_FOUND
;
// XXX errors?
ctx
->
errstr
=
"failed to find blob by id"
;
goto
done
;
}
// default with no part is the whole message
if
(
ctx
->
accept_mime
)
{
/* XXX Can we be smarter here and test against part->[sub]type ? */
buf_setcstr
(
&
ctx
->
content_type
,
ctx
->
accept_mime
);
}
if
(
part
)
{
// map into just this part
const
char
*
base
=
buf_base
(
&
ctx
->
blob
)
+
part
->
content_offset
;
size_t
len
=
part
->
content_size
;
char
*
decbuf
=
NULL
;
// binary decode if needed
int
encoding
=
part
->
charset_enc
&
0xff
;
base
=
charset_decode_mimebody
(
base
,
len
,
encoding
,
&
decbuf
,
&
len
);
if
(
!
base
)
{
res
=
HTTP_NOT_FOUND
;
// XXX errors?
ctx
->
errstr
=
"failed to decode blob"
;
goto
done
;
}
else
if
(
decbuf
)
{
buf_initm
(
&
ctx
->
blob
,
decbuf
,
len
);
buf_setcstr
(
&
ctx
->
encoding
,
"BINARY"
);
}
else
{
/* Skip headers */
buf_remove
(
&
ctx
->
blob
,
0
,
part
->
content_offset
);
buf_truncate
(
&
ctx
->
blob
,
part
->
content_size
);
buf_setcstr
(
&
ctx
->
encoding
,
part
->
encoding
);
}
}
done
:
if
(
mbox
)
jmap_closembox
(
req
,
&
mbox
);
if
(
body
)
{
message_free_body
(
body
);
free
(
body
);
}
if
(
mr
)
{
msgrecord_unref
(
&
mr
);
}
return
res
;
}
HIDDEN
int
jmap_getblob
(
jmap_req_t
*
req
,
jmap_getblob_context_t
*
ctx
)
{
int
res
=
0
;
if
(
!
ctx
->
blobid
)
return
HTTP_NOT_FOUND
;
/* Call getblob handlers */
int
i
;
for
(
i
=
0
;
i
<
ptrarray_size
(
&
my_jmap_settings
.
getblob_handlers
);
i
++
)
{
jmap_getblob_handler
*
handler
=
ptrarray_nth
(
&
my_jmap_settings
.
getblob_handlers
,
i
);
jmap_getblob_ctx_reset
(
ctx
);
res
=
handler
(
req
,
ctx
);
if
(
res
)
break
;
}
if
(
!
res
)
{
/* Try default getblob handler */
jmap_getblob_ctx_reset
(
ctx
);
res
=
jmap_getblob_default_handler
(
req
,
ctx
);
}
if
(
res
==
HTTP_OK
)
return
0
;
return
res
;
}
/* Handle a GET on the download endpoint */
static
int
jmap_download
(
struct
transaction_t
*
txn
)
{
const
char
*
userid
=
txn
->
req_tgt
.
resource
;
const
char
*
slash
=
strchr
(
userid
,
'/'
);
if
(
!
slash
)
{
/* XXX - error, needs AccountId */
return
HTTP_NOT_FOUND
;
}
const
char
*
blobbase
=
slash
+
1
;
slash
=
strchr
(
blobbase
,
'/'
);
if
(
!
slash
)
{
/* XXX - error, needs blobid */
txn
->
error
.
desc
=
"failed to find blobid"
;
return
HTTP_BAD_REQUEST
;
}
size_t
bloblen
=
slash
-
blobbase
;
const
char
*
fname
=
slash
+
1
;
/* now we're allocating memory, so don't return from here! */
char
*
accountid
=
xstrndup
(
userid
,
strchr
(
userid
,
'/'
)
-
userid
);
int
res
=
0
;
struct
conversations_state
*
cstate
=
NULL
;
int
r
=
conversations_open_user
(
accountid
,
1
/*shared*/
,
&
cstate
);
if
(
r
)
{
txn
->
error
.
desc
=
error_message
(
r
);
res
=
(
r
==
IMAP_MAILBOX_BADNAME
)
?
HTTP_NOT_FOUND
:
HTTP_SERVER_ERROR
;
free
(
accountid
);
return
res
;
}
char
*
blobid
=
NULL
;
char
*
accept_mime
=
NULL
;
/* Initialize request context */
struct
jmap_req
req
;
jmap_initreq
(
&
req
);
req
.
userid
=
httpd_userid
;
req
.
accountid
=
accountid
;
req
.
cstate
=
cstate
;
req
.
authstate
=
httpd_authstate
;
req
.
txn
=
txn
;
/* Initialize ACL mailbox cache for findblob */
hash_table
mbstates
=
HASH_TABLE_INITIALIZER
;
construct_hash_table
(
&
mbstates
,
64
,
0
);
req
.
mbstates
=
&
mbstates
;
blobid
=
xstrndup
(
blobbase
,
bloblen
);
struct
strlist
*
param
;
if
((
param
=
hash_lookup
(
"accept"
,
&
txn
->
req_qparams
)))
{
accept_mime
=
xstrdup
(
param
->
s
);
}
const
char
**
hdr
;
if
(
!
accept_mime
&&
(
hdr
=
spool_getheader
(
txn
->
req_hdrs
,
"Accept"
)))
{
accept_mime
=
parse_accept_header
(
hdr
);
}
/* Call blob download handlers */
jmap_getblob_context_t
ctx
;
jmap_getblob_ctx_init
(
&
ctx
,
accountid
,
blobid
,
accept_mime
,
1
);
res
=
jmap_getblob
(
&
req
,
&
ctx
);
if
(
res
)
{
txn
->
error
.
desc
=
ctx
.
errstr
?
ctx
.
errstr
:
error_message
(
res
);
}
else
{
/* Set Content-Disposition header */
txn
->
resp_body
.
dispo
.
attach
=
fname
!=
NULL
;
txn
->
resp_body
.
dispo
.
fname
=
fname
;
/* Set Cache-Control directives */
txn
->
resp_body
.
maxage
=
604800
;
/* 7 days */
txn
->
flags
.
cc
|=
CC_MAXAGE
|
CC_PRIVATE
|
CC_IMMUTABLE
;
/* Write body */
txn
->
resp_body
.
type
=
buf_len
(
&
ctx
.
content_type
)
?
buf_cstring
(
&
ctx
.
content_type
)
:
"application/octet-stream"
;
txn
->
resp_body
.
len
=
buf_len
(
&
ctx
.
blob
);
write_body
(
HTTP_OK
,
txn
,
buf_base
(
&
ctx
.
blob
),
buf_len
(
&
ctx
.
blob
));
}
jmap_getblob_ctx_fini
(
&
ctx
);
free_hash_table
(
&
mbstates
,
free
);
conversations_commit
(
&
cstate
);
free
(
accept_mime
);
free
(
accountid
);
free
(
blobid
);
jmap_finireq
(
&
req
);
return
res
;
}
static
int
has_shared_rw_rights_cb
(
const
mbentry_t
*
mbentry
,
void
*
vrock
)
{
int
*
rights
=
(
int
*
)
vrock
;
/* skip any special use folders */
switch
(
mbtype_isa
(
mbentry
->
mbtype
))
{
case
MBTYPE_EMAIL
:
case
MBTYPE_CALENDAR
:
case
MBTYPE_ADDRESSBOOK
:
break
;
default
:
return
0
;
}
*
rights
|=
(
httpd_myrights
(
httpd_authstate
,
mbentry
)
&
JACL_ADDITEMS
);
if
(
*
rights
)
{
/* one writable mailbox is enough to short-circuit the search */
return
CYRUSDB_DONE
;
}
return
0
;
}
/* See if this account has shared any mailbox with the authenticated user */
static
int
has_shared_rw_rights
(
const
char
*
accountid
)
{
int
rights
=
0
;
mboxlist_usermboxtree
(
accountid
,
NULL
,
&
has_shared_rw_rights_cb
,
&
rights
,
0
);
return
rights
;
}
static
int
lookup_upload_collection
(
const
char
*
accountid
,
mbentry_t
**
mbentry
)
{
mbname_t
*
mbname
;
const
char
*
uploadname
;
int
r
;
/* Create upload mailbox name from the parsed path */
mbname
=
mbname_from_userid
(
accountid
);
mbname_push_boxes
(
mbname
,
config_getstring
(
IMAPOPT_JMAPUPLOADFOLDER
));
/* XXX - hack to allow @domain parts for non-domain-split users */
if
(
httpd_extradomain
)
{
/* not allowed to be cross domain */
if
(
mbname_localpart
(
mbname
)
&&
strcmpsafe
(
mbname_domain
(
mbname
),
httpd_extradomain
))
{
r
=
HTTP_NOT_FOUND
;
goto
done
;
}
mbname_set_domain
(
mbname
,
NULL
);
}
/* Locate the mailbox */
uploadname
=
mbname_intname
(
mbname
);
r
=
proxy_mlookup
(
uploadname
,
mbentry
,
NULL
,
NULL
);
if
(
r
==
IMAP_MAILBOX_NONEXISTENT
)
{
/* Find location of INBOX */
char
*
inboxname
=
mboxname_user_mbox
(
accountid
,
NULL
);
int
r1
=
proxy_mlookup
(
inboxname
,
mbentry
,
NULL
,
NULL
);
free
(
inboxname
);
if
(
r1
==
IMAP_MAILBOX_NONEXISTENT
)
{
r
=
IMAP_INVALID_USER
;
goto
done
;
}
if
(
!
strcmp
(
accountid
,
httpd_userid
))
{
int
rights
=
httpd_myrights
(
httpd_authstate
,
*
mbentry
);
if
(
!
(
rights
&
ACL_CREATE
))
{
r
=
IMAP_PERMISSION_DENIED
;
goto
done
;
}
}
mboxlist_entry_free
(
mbentry
);
*
mbentry
=
mboxlist_entry_create
();
(
*
mbentry
)
->
name
=
xstrdup
(
uploadname
);
(
*
mbentry
)
->
mbtype
=
MBTYPE_COLLECTION
;
}
else
if
(
!
r
)
{
int
rights
=
httpd_myrights
(
httpd_authstate
,
*
mbentry
);
if
(
!
(
rights
&
ACL_INSERT
))
{
r
=
IMAP_PERMISSION_DENIED
;
goto
done
;
}
}
done
:
mbname_free
(
&
mbname
);
return
r
;
}
static
int
_create_upload_collection
(
const
char
*
accountid
,
struct
mailbox
**
mailbox
)
{
/* upload collection */
struct
mboxlock
*
namespacelock
=
user_namespacelock
(
accountid
);
mbentry_t
*
mbentry
=
NULL
;
int
r
=
lookup_upload_collection
(
accountid
,
&
mbentry
);
if
(
r
==
IMAP_INVALID_USER
)
{
goto
done
;
}
else
if
(
r
==
IMAP_PERMISSION_DENIED
)
{
if
(
has_shared_rw_rights
(
accountid
))
{
/* add rights for the sharee */
char
rightstr
[
100
];
cyrus_acl_masktostr
(
JACL_READITEMS
|
JACL_WRITE
,
rightstr
);
r
=
mboxlist_setacl
(
&
jmap_namespace
,
mbentry
->
name
,
httpd_userid
,
rightstr
,
1
,
httpd_userid
,
httpd_authstate
);
}
if
(
r
)
goto
done
;
}
else
if
(
r
==
IMAP_MAILBOX_NONEXISTENT
)
{
if
(
!
mbentry
)
goto
done
;
else
if
(
mbentry
->
server
)
{
proxy_findserver
(
mbentry
->
server
,
&
http_protocol
,
httpd_userid
,
&
backend_cached
,
NULL
,
NULL
,
httpd_in
);
goto
done
;
}
int
is_shared
=
0
;
if
(
strcmp
(
accountid
,
httpd_userid
))
{
if
(
!
(
has_shared_rw_rights
(
accountid
)))
{
r
=
IMAP_PERMISSION_DENIED
;
goto
done
;
}
is_shared
=
1
;
}
r
=
mboxlist_createmailbox
(
mbentry
,
0
/*options*/
,
0
/*highestmodseq*/
,
1
/*isadmin*/
,
accountid
,
httpd_authstate
,
0
/*flags*/
,
mailbox
);
/* we lost the race, that's OK */
if
(
r
==
IMAP_MAILBOX_LOCKED
)
r
=
0
;
else
{
if
(
r
)
{
syslog
(
LOG_ERR
,
"IOERROR: failed to create %s (%s)"
,
mbentry
->
name
,
error_message
(
r
));
}
else
if
(
is_shared
)
{
/* add rights for the sharee */
char
*
newacl
=
xstrdupnull
(
mailbox_acl
(
*
mailbox
));
cyrus_acl_set
(
&
newacl
,
httpd_userid
,
ACL_MODE_SET
,
JACL_READITEMS
|
JACL_WRITE
,
NULL
,
NULL
);
/* ok, change the mailboxes database */
r
=
mboxlist_sync_setacls
(
mbentry
->
name
,
newacl
,
mailbox_modseq_dirty
(
*
mailbox
));
if
(
r
)
{
syslog
(
LOG_ERR
,
"mboxlist_sync_setacls(%s) failed: %s"
,
mbentry
->
name
,
error_message
(
r
));
}
else
{
/* ok, change the backup in cyrus.header */
mailbox_set_acl
(
*
mailbox
,
newacl
);
}
free
(
newacl
);
}
goto
done
;
}
}
else
if
(
r
)
goto
done
;
if
(
mailbox
)
{
/* Open mailbox for writing */
r
=
mailbox_open_iwl
(
mbentry
->
name
,
mailbox
);
if
(
r
)
{
syslog
(
LOG_ERR
,
"mailbox_open_iwl(%s) failed: %s"
,
mbentry
->
name
,
error_message
(
r
));
}
}
done
:
mboxname_release
(
&
namespacelock
);
mboxlist_entry_free
(
&
mbentry
);
return
r
;
}
HIDDEN
int
jmap_open_upload_collection
(
const
char
*
accountid
,
struct
mailbox
**
mailbox
)
{
/* upload collection */
mbentry_t
*
mbentry
=
NULL
;
int
r
=
lookup_upload_collection
(
accountid
,
&
mbentry
);
if
(
r
)
{
mboxlist_entry_free
(
&
mbentry
);
return
_create_upload_collection
(
accountid
,
mailbox
);
}
if
(
mailbox
)
{
/* Open mailbox for writing */
r
=
mailbox_open_iwl
(
mbentry
->
name
,
mailbox
);
if
(
r
)
{
syslog
(
LOG_ERR
,
"mailbox_open_iwl(%s) failed: %s"
,
mbentry
->
name
,
error_message
(
r
));
}
}
mboxlist_entry_free
(
&
mbentry
);
return
r
;
}
/* Helper function to determine domain of data */
enum
{
DOMAIN_7BIT
=
0
,
DOMAIN_8BIT
,
DOMAIN_BINARY
};
static
int
data_domain
(
const
char
*
p
,
size_t
n
)
{
int
r
=
DOMAIN_7BIT
;
while
(
n
--
)
{
if
(
!*
p
)
return
DOMAIN_BINARY
;
if
(
*
p
&
0x80
)
r
=
DOMAIN_8BIT
;
p
++
;
}
return
r
;
}
/* Handle a POST on the upload endpoint */
static
int
jmap_upload
(
struct
transaction_t
*
txn
)
{
strarray_t
flags
=
STRARRAY_INITIALIZER
;
struct
body
*
body
=
NULL
;
int
ret
=
HTTP_SERVER_ERROR
;
json_t
*
resp
=
NULL
;
hdrcache_t
hdrcache
=
txn
->
req_hdrs
;
struct
stagemsg
*
stage
=
NULL
;
FILE
*
f
=
NULL
;
const
char
**
hdr
;
time_t
now
=
time
(
NULL
);
struct
appendstate
as
;
char
*
normalisedtype
=
NULL
;
int
rawmessage
=
0
;
struct
mailbox
*
mailbox
=
NULL
;
int
r
=
0
;
/* Read body */
txn
->
req_body
.
flags
|=
BODY_DECODE
;
r
=
http_read_req_body
(
txn
);
if
(
r
)
{
txn
->
flags
.
conn
=
CONN_CLOSE
;
return
r
;
}
const
char
*
data
=
buf_base
(
&
txn
->
req_body
.
payload
);
size_t
datalen
=
buf_len
(
&
txn
->
req_body
.
payload
);
if
(
datalen
>
(
size_t
)
my_jmap_settings
.
limits
[
MAX_SIZE_UPLOAD
])
{
txn
->
error
.
desc
=
"JSON upload byte size exceeds maxSizeUpload"
;
return
HTTP_PAYLOAD_TOO_LARGE
;
}
/* Resource must be {accountId}/ with no trailing path */
char
*
accountid
=
xstrdup
(
txn
->
req_tgt
.
resource
);
char
*
slash
=
strchr
(
accountid
,
'/'
);
if
(
!
slash
||
*
(
slash
+
1
)
!=
'\0'
)
{
ret
=
HTTP_NOT_FOUND
;
goto
done
;
}
*
slash
=
'\0'
;
r
=
jmap_open_upload_collection
(
accountid
,
&
mailbox
);
if
(
r
)
{
syslog
(
LOG_ERR
,
"jmap_upload: can't open upload collection for %s: %s"
,
error_message
(
r
),
accountid
);
ret
=
HTTP_NOT_FOUND
;
goto
done
;
}
/* Prepare to stage the message */
if
(
!
(
f
=
append_newstage
(
mailbox_name
(
mailbox
),
now
,
0
,
&
stage
)))
{
syslog
(
LOG_ERR
,
"append_newstage(%s) failed"
,
mailbox_name
(
mailbox
));
txn
->
error
.
desc
=
"append_newstage() failed"
;
ret
=
HTTP_SERVER_ERROR
;
goto
done
;
}
const
char
*
type
=
"application/octet-stream"
;
if
((
hdr
=
spool_getheader
(
hdrcache
,
"Content-Type"
)))
{
type
=
hdr
[
0
];
}
/* Remove CFWS and encodings from type */
normalisedtype
=
charset_decode_mimeheader
(
type
,
CHARSET_KEEPCASE
);
if
(
!
strcasecmp
(
normalisedtype
,
"message/rfc822"
))
{
struct
protstream
*
stream
=
prot_readmap
(
data
,
datalen
);
r
=
message_copy_strict
(
stream
,
f
,
datalen
,
0
);
prot_free
(
stream
);
if
(
!
r
)
{
rawmessage
=
1
;
goto
wrotebody
;
}
// otherwise we gotta clean up and make it an attachment
if
(
ftruncate
(
fileno
(
f
),
0L
)
<
0
||
fseek
(
f
,
0L
,
SEEK_SET
)
<
0
)
{
syslog
(
LOG_ERR
,
"IOERROR: failed to reset file in JMAP upload"
);
}
}
/* Create RFC 5322 header for resource */
if
((
hdr
=
spool_getheader
(
hdrcache
,
"User-Agent"
)))
{
fprintf
(
f
,
"User-Agent: %s
\r\n
"
,
hdr
[
0
]);
}
if
((
hdr
=
spool_getheader
(
hdrcache
,
"From"
)))
{
fprintf
(
f
,
"From: %s
\r\n
"
,
hdr
[
0
]);
}
else
{
char
*
mimehdr
;
assert
(
!
buf_len
(
&
txn
->
buf
));
if
(
strchr
(
httpd_userid
,
'@'
))
{
/* XXX This needs to be done via an LDAP/DB lookup */
buf_printf
(
&
txn
->
buf
,
"<%s>"
,
httpd_userid
);
}
else
{
buf_printf
(
&
txn
->
buf
,
"<%s@%s>"
,
httpd_userid
,
config_servername
);
}
mimehdr
=
charset_encode_mimeheader
(
buf_cstring
(
&
txn
->
buf
),
buf_len
(
&
txn
->
buf
),
0
);
fprintf
(
f
,
"From: %s
\r\n
"
,
mimehdr
);
free
(
mimehdr
);
buf_reset
(
&
txn
->
buf
);
}
if
((
hdr
=
spool_getheader
(
hdrcache
,
"Subject"
)))
{
fprintf
(
f
,
"Subject: %s
\r\n
"
,
hdr
[
0
]);
}
if
((
hdr
=
spool_getheader
(
hdrcache
,
"Date"
)))
{
fprintf
(
f
,
"Date: %s
\r\n
"
,
hdr
[
0
]);
}
else
{
char
datestr
[
80
];
time_to_rfc5322
(
now
,
datestr
,
sizeof
(
datestr
));
fprintf
(
f
,
"Date: %s
\r\n
"
,
datestr
);
}
if
((
hdr
=
spool_getheader
(
hdrcache
,
"Message-ID"
)))
{
fprintf
(
f
,
"Message-ID: %s
\r\n
"
,
hdr
[
0
]);
}
fprintf
(
f
,
"Content-Type: %s
\r\n
"
,
type
);
int
domain
=
data_domain
(
data
,
datalen
);
switch
(
domain
)
{
case
DOMAIN_BINARY
:
fputs
(
"Content-Transfer-Encoding: BINARY
\r\n
"
,
f
);
break
;
case
DOMAIN_8BIT
:
fputs
(
"Content-Transfer-Encoding: 8BIT
\r\n
"
,
f
);
break
;
default
:
break
;
// no CTE == 7bit
}
if
((
hdr
=
spool_getheader
(
hdrcache
,
"Content-Disposition"
)))
{
fprintf
(
f
,
"Content-Disposition: %s
\r\n
"
,
hdr
[
0
]);
}
if
((
hdr
=
spool_getheader
(
hdrcache
,
"Content-Description"
)))
{
fprintf
(
f
,
"Content-Description: %s
\r\n
"
,
hdr
[
0
]);
}
fprintf
(
f
,
"Content-Length: %u
\r\n
"
,
(
unsigned
)
datalen
);
fputs
(
"MIME-Version: 1.0
\r\n\r\n
"
,
f
);
/* Write the data to the file */
fwrite
(
data
,
datalen
,
1
,
f
);
wrotebody
:
fclose
(
f
);
/* Prepare to append the message to the mailbox */
r
=
append_setup_mbox
(
&
as
,
mailbox
,
httpd_userid
,
httpd_authstate
,
0
,
/*quota*/
NULL
,
0
,
0
,
/*event*/
0
);
if
(
r
)
{
syslog
(
LOG_ERR
,
"append_setup(%s) failed: %s"
,
mailbox_name
(
mailbox
),
error_message
(
r
));
ret
=
HTTP_SERVER_ERROR
;
txn
->
error
.
desc
=
"append_setup() failed"
;
goto
done
;
}
/* Append the message to the mailbox */
strarray_append
(
&
flags
,
"
\\
Deleted"
);
strarray_append
(
&
flags
,
"
\\
Expunged"
);
// custom flag to insta-expunge!
r
=
append_fromstage
(
&
as
,
&
body
,
stage
,
now
,
0
,
&
flags
,
0
,
/*annots*/
NULL
);
if
(
r
)
{
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"
;
goto
done
;
}
r
=
append_commit
(
&
as
);
if
(
r
)
{
syslog
(
LOG_ERR
,
"append_commit(%s) failed: %s"
,
mailbox_name
(
mailbox
),
error_message
(
r
));
ret
=
HTTP_SERVER_ERROR
;
txn
->
error
.
desc
=
"append_commit() failed"
;
goto
done
;
}
char
datestr
[
RFC3339_DATETIME_MAX
];
time_to_rfc3339
(
now
+
86400
,
datestr
,
RFC3339_DATETIME_MAX
);
char
blob_id
[
JMAP_BLOBID_SIZE
];
jmap_set_blobid
(
rawmessage
?
&
body
->
guid
:
&
body
->
content_guid
,
blob_id
);
/* Create response object */
resp
=
json_pack
(
"{s:s}"
,
"accountId"
,
accountid
);
json_object_set_new
(
resp
,
"blobId"
,
json_string
(
blob_id
));
json_object_set_new
(
resp
,
"size"
,
json_integer
(
datalen
));
json_object_set_new
(
resp
,
"expires"
,
json_string
(
datestr
));
json_object_set_new
(
resp
,
"type"
,
json_string
(
normalisedtype
));
done
:
free
(
normalisedtype
);
free
(
accountid
);
if
(
body
)
{
message_free_body
(
body
);
free
(
body
);
}
strarray_fini
(
&
flags
);
append_removestage
(
stage
);
if
(
mailbox
)
{
if
(
r
)
mailbox_abort
(
mailbox
);
else
r
=
mailbox_commit
(
mailbox
);
mailbox_close
(
&
mailbox
);
}
/* ensure we didn't leak anything! */
assert
(
!
open_mailboxes_exist
());
assert
(
!
open_mboxlocks_exist
());
// checkpoint before replying
sync_checkpoint
(
httpd_in
);
/* Output the JSON object */
if
(
resp
)
ret
=
json_response
(
HTTP_CREATED
,
txn
,
resp
);
return
ret
;
}
/* Handle a GET on the session endpoint */
static
int
jmap_get_session
(
struct
transaction_t
*
txn
)
{
json_t
*
jsession
=
json_object
();
/* URLs */
json_object_set_new
(
jsession
,
"username"
,
json_string
(
httpd_userid
));
json_object_set_new
(
jsession
,
"apiUrl"
,
json_string
(
JMAP_BASE_URL
));
json_object_set_new
(
jsession
,
"downloadUrl"
,
json_string
(
JMAP_BASE_URL
JMAP_DOWNLOAD_COL
JMAP_DOWNLOAD_TPL
));
json_object_set_new
(
jsession
,
"uploadUrl"
,
json_string
(
JMAP_BASE_URL
JMAP_UPLOAD_COL
JMAP_UPLOAD_TPL
));
json_object_set_new
(
jsession
,
"eventSourceUrl"
,
json_string
(
JMAP_BASE_URL
JMAP_EVENTSOURCE_COL
JMAP_EVENTSOURCE_TPL
));
/* state */
char
*
inboxname
=
mboxname_user_mbox
(
httpd_userid
,
NULL
);
struct
buf
state
=
BUF_INITIALIZER
;
buf_printf
(
&
state
,
MODSEQ_FMT
,
mboxname_readraclmodseq
(
inboxname
));
json_object_set_new
(
jsession
,
"state"
,
json_string
(
buf_cstring
(
&
state
)));
free
(
inboxname
);
buf_free
(
&
state
);
/* capabilities */
json_object_set
(
jsession
,
"capabilities"
,
my_jmap_settings
.
server_capabilities
);
json_t
*
accounts
=
json_object
();
json_t
*
primary_accounts
=
json_object
();
jmap_accounts
(
accounts
,
primary_accounts
);
json_object_set_new
(
jsession
,
"accounts"
,
accounts
);
json_object_set_new
(
jsession
,
"primaryAccounts"
,
primary_accounts
);
/* Response should not be cached */
txn
->
flags
.
cc
|=
CC_NOCACHE
|
CC_NOSTORE
|
CC_REVALIDATE
;
/* Write the JSON response */
return
json_response
(
HTTP_OK
,
txn
,
jsession
);
}
static
void
buf_appendjson
(
struct
buf
*
buf
,
json_t
*
json
,
size_t
flags
)
{
/* Determine length of JSON encoding */
size_t
size
=
json_dumpb
(
json
,
NULL
,
0
,
flags
);
/* Grow the buffer */
buf_ensure
(
buf
,
size
);
/* Append the JSON encoding */
size
=
json_dumpb
(
json
,
(
char
*
)
buf_base
(
buf
)
+
buf_len
(
buf
),
size
,
flags
);
/* Set the new buffer length */
buf_truncate
(
buf
,
buf_len
(
buf
)
+
size
);
}
static
struct
prot_waitevent
*
ws_push
(
struct
protstream
*
s
__attribute__
((
unused
)),
struct
prot_waitevent
*
ev
,
void
*
rock
)
{
struct
transaction_t
*
txn
=
(
struct
transaction_t
*
)
rock
;
jmap_push_ctx_t
*
jpush
=
(
jmap_push_ctx_t
*
)
txn
->
push_ctx
;
json_t
*
jstate
=
jmap_push_get_state
(
jpush
);
struct
buf
*
buf
=
&
jpush
->
buf
;
xsyslog
(
LOG_DEBUG
,
"JMAP WS push"
,
"accountid=<%s>"
,
jpush
->
accountid
);
if
(
jstate
)
{
/* Add pushState */
buf_reset
(
buf
);
buf_printf
(
buf
,
MODSEQ_FMT
,
jpush
->
counters
.
highestmodseq
);
json_object_set_new
(
jstate
,
"pushState"
,
json_string
(
buf_cstring
(
buf
)));
/* Send the StateChange object */
size_t
flags
=
JSON_PRESERVE_ORDER
|
(
config_httpprettytelemetry
?
JSON_INDENT
(
2
)
:
JSON_COMPACT
);
buf_reset
(
buf
);
buf_appendjson
(
buf
,
jstate
,
flags
);
ws_send
(
txn
,
buf
);
prot_flush
(
txn
->
conn
->
pout
);
}
json_decref
(
jstate
);
/* Schedule our next event */
ev
->
mark
=
time
(
NULL
)
+
jmap_push_poll
;
return
ev
;
}
static
void
ws_push_enable
(
struct
transaction_t
*
txn
,
json_t
*
req
)
{
jmap_push_ctx_t
*
jpush
=
(
jmap_push_ctx_t
*
)
txn
->
push_ctx
;
json_t
*
jtypes
=
json_object_get
(
req
,
"dataTypes"
);
json_t
*
pushState
=
json_object_get
(
req
,
"pushState"
);
strarray_t
types
=
STRARRAY_INITIALIZER
;
modseq_t
lastmodseq
=
ULLONG_MAX
;
xsyslog
(
LOG_DEBUG
,
"JMAP WS push enable"
,
"jpush=<%p>, jtypes=<%p>"
,
jpush
,
jtypes
);
if
(
!
json_array_size
(
jtypes
))
{
jmap_push_done
(
txn
);
return
;
}
size_t
i
;
json_t
*
jval
;
json_array_foreach
(
jtypes
,
i
,
jval
)
{
strarray_append
(
&
types
,
json_string_value
(
jval
));
}
if
(
json_is_string
(
pushState
))
{
lastmodseq
=
atomodseq_t
(
json_string_value
(
pushState
));
}
jpush
=
jmap_push_init
(
txn
,
httpd_userid
,
&
types
,
lastmodseq
,
&
ws_push
);
if
(
jpush
&&
(
lastmodseq
<
ULLONG_MAX
))
{
/* Send all changes now */
ws_push
(
txn
->
conn
->
pin
,
jpush
->
wait
,
txn
);
}
strarray_fini
(
&
types
);
}
/*
* WebSockets data callback ('jmap' sub-protocol): Process JMAP API request.
*
* Can be tested with:
* https://github.com/websockets/wscat
* https://chrome.google.com/webstore/detail/web-socket-client/lifhekgaodigcpmnakfhaaaboididbdn
*
* WebSockets over HTTP/2 currently only available in Chrome:
* https://www.chromestatus.com/feature/6251293127475200
* (using --enable-experimental-web-platform-features)
*/
static
int
jmap_ws
(
struct
transaction_t
*
txn
,
enum
wslay_opcode
opcode
,
struct
buf
*
inbuf
,
struct
buf
*
outbuf
,
struct
buf
*
logbuf
)
{
json_t
*
req
=
NULL
,
*
res
=
NULL
;
int
ret
;
if
(
opcode
!=
WSLAY_TEXT_FRAME
)
{
/* Only accept text frames */
return
HTTP_NOT_ACCEPTABLE
;
}
/* Set request payload */
buf_init_ro
(
&
txn
->
req_body
.
payload
,
buf_base
(
inbuf
),
buf_len
(
inbuf
));
/* Always start with fresh working buffer */
buf_reset
(
&
txn
->
buf
);
/* Parse the JSON request */
ret
=
parse_json_body
(
txn
,
&
req
);
if
(
ret
)
{
ret
=
jmap_error_response
(
txn
,
ret
,
&
res
);
}
else
{
const
char
*
type
=
json_string_value
(
json_object_get
(
req
,
"@type"
));
if
(
!
strcmpsafe
(
type
,
"Request"
))
{
/* Process the API request */
ret
=
jmap_api
(
txn
,
req
,
&
res
,
&
my_jmap_settings
);
}
else
if
(
!
strcmpsafe
(
type
,
"WebSocketPushEnable"
))
{
/* Log request */
spool_replace_header
(
xstrdup
(
":jmap"
),
xstrdup
(
"WebSocketPushEnable"
),
txn
->
req_hdrs
);
ws_push_enable
(
txn
,
req
);
ret
=
HTTP_NO_CONTENT
;
}
else
if
(
!
strcmpsafe
(
type
,
"WebSocketPushDisable"
))
{
/* Log request */
spool_replace_header
(
xstrdup
(
":jmap"
),
xstrdup
(
"WebSocketPushDisable"
),
txn
->
req_hdrs
);
jmap_push_done
(
txn
);
ret
=
HTTP_NO_CONTENT
;
}
else
{
buf_reset
(
&
txn
->
buf
);
buf_printf
(
&
txn
->
buf
,
"Unknown request @type: %s"
,
type
?
type
:
"null"
);
txn
->
error
.
desc
=
buf_cstring
(
&
txn
->
buf
);
ret
=
jmap_error_response
(
txn
,
JMAP_NOT_REQUEST
,
&
res
);
}
}
/* ensure we didn't leak anything! */
assert
(
!
open_mailboxes_exist
());
assert
(
!
open_mboxlocks_exist
());
// checkpoint before we reply
sync_checkpoint
(
httpd_in
);
/* Free request payload */
buf_free
(
&
txn
->
req_body
.
payload
);
if
(
logbuf
)
{
/* Log JMAP methods */
const
char
**
hdr
=
spool_getheader
(
txn
->
req_hdrs
,
":jmap"
);
if
(
hdr
)
buf_printf
(
logbuf
,
"; jmap=%s"
,
hdr
[
0
]);
/* Add logheaders */
if
(
strarray_size
(
httpd_log_headers
))
{
json_t
*
jlogHeaders
=
json_object_get
(
req
,
"logHeaders"
);
const
char
*
hdrname
;
json_t
*
jval
;
json_object_foreach
(
jlogHeaders
,
hdrname
,
jval
)
{
const
char
*
val
=
json_string_value
(
jval
);
if
(
val
&&
strarray_find_case
(
httpd_log_headers
,
hdrname
,
0
)
>=
0
)
{
buf_printf
(
logbuf
,
"; %s=
\"
%s
\"
"
,
hdrname
,
val
);
}
}
}
}
if
(
res
)
{
/* Add @type */
json_object_set_new
(
res
,
"@type"
,
json_string
(
ret
?
"RequestError"
:
"Response"
));
/* Add requestId */
json_t
*
id
=
json_object_get
(
req
,
"id"
);
if
(
id
)
{
json_object_set
(
res
,
"requestId"
,
id
);
}
/* Return the JSON object */
ret
=
json_response
(
0
,
txn
,
res
);
buf_move
(
outbuf
,
&
txn
->
resp_body
.
payload
);
}
/* Clear error string */
txn
->
error
.
desc
=
NULL
;
json_decref
(
req
);
return
ret
;
}
static
struct
prot_waitevent
*
es_push
(
struct
protstream
*
s
__attribute__
((
unused
)),
struct
prot_waitevent
*
ev
,
void
*
rock
)
{
struct
transaction_t
*
txn
=
(
struct
transaction_t
*
)
rock
;
jmap_push_ctx_t
*
jpush
=
(
jmap_push_ctx_t
*
)
txn
->
push_ctx
;
json_t
*
jstate
=
NULL
;
struct
buf
*
buf
=
&
jpush
->
buf
;
const
char
*
event
=
NULL
;
time_t
now
=
time
(
NULL
);
int
do_close
=
0
;
xsyslog
(
LOG_DEBUG
,
"JMAP eventSource push"
,
"accountid=<%s>, now=<%ld>, next_poll=<%ld>, next_ping=<%ld>"
,
jpush
->
accountid
,
now
,
jpush
->
next_poll
,
jpush
->
next_ping
);
buf_reset
(
buf
);
if
(
now
>=
jpush
->
next_poll
)
{
jstate
=
jmap_push_get_state
(
jpush
);
jpush
->
next_poll
=
now
+
jmap_push_poll
;
if
(
jstate
)
{
/* 'state' event */
event
=
"state"
;
buf_reset
(
buf
);
buf_printf
(
buf
,
"id: "
MODSEQ_FMT
"
\n
"
,
jpush
->
counters
.
highestmodseq
);
if
(
jpush
->
closeafter
)
{
do_close
=
1
;
}
}
}
else
{
/* 'ping' event */
event
=
"ping"
;
jstate
=
json_pack
(
"{ s:i }"
,
"interval"
,
jpush
->
ping
);
}
if
(
jstate
)
{
/* Build the response */
buf_printf
(
buf
,
"event: %s
\n
data: "
,
event
);
buf_appendjson
(
buf
,
jstate
,
JSON_PRESERVE_ORDER
|
JSON_COMPACT
);
buf_appendcstr
(
buf
,
"
\n\n
"
);
/* Send the response */
write_body
(
0
,
txn
,
buf_base
(
buf
),
buf_len
(
buf
));
prot_flush
(
txn
->
conn
->
pout
);
/* Cleanup */
json_decref
(
jstate
);
if
(
do_close
)
{
jmap_push_done
(
txn
);
if
(
txn
->
flags
.
ver
>=
VER_2
)
{
/* End of stream */
write_body
(
0
,
txn
,
NULL
,
0
);
prot_flush
(
txn
->
conn
->
pout
);
}
else
{
/* Force exit out of blocking read */
shutdown
(
txn
->
conn
->
pin
->
fd
,
SHUT_RD
);
}
return
NULL
;
}
jpush
->
next_ping
=
(
jpush
->
ping
>
0
)
?
now
+
jpush
->
ping
:
INT_MAX
;
}
/* Schedule our next event */
ev
->
mark
=
MIN
(
jpush
->
next_ping
,
jpush
->
next_poll
);
return
ev
;
}
/* Handle a GET on the eventsource endpoint */
static
int
jmap_eventsource
(
struct
transaction_t
*
txn
)
{
if
(
!
jmap_push_poll
)
return
HTTP_NO_CONTENT
;
jmap_push_ctx_t
*
jpush
=
NULL
;
modseq_t
lastmodseq
=
ULLONG_MAX
;
time_t
now
=
time
(
NULL
);
const
char
**
hdr
;
if
(
txn
->
req_hdrs
&&
(
hdr
=
spool_getheader
(
txn
->
req_hdrs
,
"Last-Event-Id"
)))
{
lastmodseq
=
atomodseq_t
(
*
hdr
);
}
struct
strlist
*
param
;
if
((
param
=
hash_lookup
(
"types"
,
&
txn
->
req_qparams
)))
{
strarray_t
*
types
=
strarray_split
(
param
->
s
,
","
,
STRARRAY_TRIM
);
jpush
=
jmap_push_init
(
txn
,
httpd_userid
,
types
,
lastmodseq
,
&
es_push
);
strarray_free
(
types
);
}
if
(
!
jpush
)
{
return
HTTP_NO_CONTENT
;
}
if
((
param
=
hash_lookup
(
"closeafter"
,
&
txn
->
req_qparams
))
&&
!
strcmpsafe
(
param
->
s
,
"state"
))
{
jpush
->
closeafter
=
1
;
}
if
((
param
=
hash_lookup
(
"ping"
,
&
txn
->
req_qparams
)))
{
jpush
->
ping
=
atoi
(
param
->
s
);
}
jpush
->
next_ping
=
(
jpush
->
ping
>
0
)
?
now
+
jpush
->
ping
:
INT_MAX
;
txn
->
meth
=
METH_CONNECT
;
/* Suppress Content-Length & Accept-Ranges */
txn
->
resp_body
.
type
=
"text/event-stream"
;
txn
->
flags
.
conn
=
CONN_KEEPALIVE
;
txn
->
flags
.
cc
=
CC_NOCACHE
;
if
(
txn
->
flags
.
ver
>=
VER_2
)
{
/* Prevent end of stream */
txn
->
flags
.
te
=
TE_CHUNKED
;
}
response_header
(
HTTP_OK
,
txn
);
if
(
lastmodseq
<
ULLONG_MAX
)
{
/* Send all changes now */
jpush
->
next_poll
=
now
;
}
else
{
jpush
->
next_poll
=
now
+
jmap_push_poll
;
}
/* Schedule our first event */
jpush
->
wait
->
mark
=
MIN
(
jpush
->
next_ping
,
jpush
->
next_poll
);
if
(
txn
->
flags
.
ver
<
VER_2
)
{
/* Block until we timeout or get unexpected input */
prot_peek
(
txn
->
conn
->
pin
);
txn
->
flags
.
conn
|=
CONN_CLOSE
;
}
return
0
;
}
File Metadata
Details
Attached
Mime Type
text/x-c
Expires
Sat, Apr 4, 2:21 AM (1 w, 2 d ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18822126
Default Alt Text
http_jmap.c (46 KB)
Attached To
Mode
R111 cyrus-imapd
Attached
Detach File
Event Timeline