2020-08-05 22:08:55 +05:30
/ *
Copyright 2020 Bruno Windels < bruno @ windels . cloud >
2020-09-22 17:10:38 +05:30
Copyright 2020 The Matrix . org Foundation C . I . C .
2020-08-05 22:08:55 +05:30
Licensed under the Apache License , Version 2.0 ( the "License" ) ;
you may not use this file except in compliance with the License .
You may obtain a copy of the License at
http : //www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing , software
distributed under the License is distributed on an "AS IS" BASIS ,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND , either express or implied .
See the License for the specific language governing permissions and
limitations under the License .
* /
2020-11-06 01:54:14 +05:30
import { HomeServerError , ConnectionError } from "../error.js" ;
2020-09-22 17:10:38 +05:30
import { encodeQueryParams } from "./common.js" ;
2019-02-11 01:55:29 +05:30
2019-02-05 03:56:24 +05:30
class RequestWrapper {
2021-02-12 23:05:33 +05:30
constructor ( method , url , requestResult , log ) {
this . _log = log ;
2019-12-23 18:58:27 +05:30
this . _requestResult = requestResult ;
2020-08-05 21:06:44 +05:30
this . _promise = requestResult . response ( ) . then ( response => {
2021-02-12 23:05:33 +05:30
log ? . set ( "status" , response . status ) ;
2019-12-23 18:58:27 +05:30
// ok?
if ( response . status >= 200 && response . status < 300 ) {
2021-02-12 23:05:33 +05:30
log ? . finish ( ) ;
2019-12-23 18:58:27 +05:30
return response . body ;
} else {
2021-03-09 17:21:34 +05:30
if ( response . status >= 500 ) {
const err = new ConnectionError ( ` Internal Server Error ` ) ;
log ? . catch ( err ) ;
throw err ;
} else if ( response . status >= 400 && ! response . body ? . errcode ) {
2021-02-12 23:05:33 +05:30
const err = new ConnectionError ( ` HTTP error status ${ response . status } without errcode in body, assume this is a load balancer complaining the server is offline. ` ) ;
log ? . catch ( err ) ;
throw err ;
2020-11-06 01:54:14 +05:30
} else {
2021-02-12 23:05:33 +05:30
const err = new HomeServerError ( method , url , response . body , response . status ) ;
2021-03-09 17:21:34 +05:30
log ? . set ( "errcode" , err . errcode ) ;
2021-02-12 23:05:33 +05:30
log ? . catch ( err ) ;
throw err ;
2019-12-23 18:58:27 +05:30
}
}
2020-11-06 03:09:32 +05:30
} , err => {
// if this._requestResult is still set, the abort error came not from calling abort here
if ( err . name === "AbortError" && this . _requestResult ) {
2021-02-12 23:05:33 +05:30
const err = new Error ( ` Unexpectedly aborted, see #187. ` ) ;
log ? . catch ( err ) ;
throw err ;
2020-11-06 03:09:32 +05:30
} else {
2021-03-08 20:05:34 +05:30
if ( err . name === "ConnectionError" ) {
log ? . set ( "timeout" , err . isTimeout ) ;
}
2021-02-12 23:05:33 +05:30
log ? . catch ( err ) ;
2020-11-06 03:09:32 +05:30
throw err ;
}
2019-12-23 18:58:27 +05:30
} ) ;
2019-06-27 02:01:36 +05:30
}
2018-12-21 19:05:24 +05:30
2019-06-27 02:01:36 +05:30
abort ( ) {
2020-11-06 03:09:32 +05:30
if ( this . _requestResult ) {
2021-02-12 23:05:33 +05:30
this . _log ? . set ( "aborted" , true ) ;
2020-11-06 03:09:32 +05:30
this . _requestResult . abort ( ) ;
// to mark that it was on purpose in above rejection handler
this . _requestResult = null ;
}
2019-06-27 02:01:36 +05:30
}
2018-12-21 19:05:24 +05:30
2019-06-27 02:01:36 +05:30
response ( ) {
return this . _promise ;
}
2018-12-21 19:05:24 +05:30
}
2020-11-11 15:15:23 +05:30
function encodeBody ( body ) {
if ( body . nativeBlob && body . mimeType ) {
const blob = body ;
return {
mimeType : blob . mimeType ,
body : blob , // will be unwrapped in request fn
length : blob . size
} ;
} else if ( typeof body === "object" ) {
const json = JSON . stringify ( body ) ;
return {
mimeType : "application/json" ,
body : json ,
length : body . length
} ;
} else {
throw new Error ( "Unknown body type: " + body ) ;
}
}
2020-04-21 00:56:39 +05:30
export class HomeServerApi {
2020-04-05 18:41:15 +05:30
constructor ( { homeServer , accessToken , request , createTimeout , reconnector } ) {
2019-03-09 00:33:47 +05:30
// store these both in a closure somehow so it's harder to get at in case of XSS?
// one could change the homeserver as well so the token gets sent there, so both must be protected from read/write
2019-12-23 18:58:27 +05:30
this . _homeserver = homeServer ;
2019-06-27 02:01:36 +05:30
this . _accessToken = accessToken ;
2019-12-23 18:58:27 +05:30
this . _requestFn = request ;
2020-04-05 18:41:15 +05:30
this . _createTimeout = createTimeout ;
this . _reconnector = reconnector ;
2019-06-27 02:01:36 +05:30
}
2018-12-21 19:05:24 +05:30
2019-06-27 02:01:36 +05:30
_url ( csPath ) {
return ` ${ this . _homeserver } /_matrix/client/r0 ${ csPath } ` ;
}
2018-12-21 19:05:24 +05:30
2020-09-18 21:43:20 +05:30
_baseRequest ( method , url , queryParams , body , options , accessToken ) {
2020-08-20 19:10:43 +05:30
const queryString = encodeQueryParams ( queryParams ) ;
2020-03-31 03:26:03 +05:30
url = ` ${ url } ? ${ queryString } ` ;
2021-02-12 23:05:33 +05:30
let log ;
if ( options ? . log ) {
const parent = options ? . log ;
log = parent . child ( {
2021-02-17 23:15:12 +05:30
t : "network" ,
2021-02-12 23:05:33 +05:30
url ,
method ,
} , parent . level . Info ) ;
}
2020-11-11 15:15:23 +05:30
let encodedBody ;
2020-04-23 00:16:47 +05:30
const headers = new Map ( ) ;
2020-09-18 21:43:20 +05:30
if ( accessToken ) {
headers . set ( "Authorization" , ` Bearer ${ accessToken } ` ) ;
2019-06-27 02:01:36 +05:30
}
2020-04-23 00:16:47 +05:30
headers . set ( "Accept" , "application/json" ) ;
2019-06-27 02:01:36 +05:30
if ( body ) {
2020-11-11 15:15:23 +05:30
const encoded = encodeBody ( body ) ;
headers . set ( "Content-Type" , encoded . mimeType ) ;
headers . set ( "Content-Length" , encoded . length ) ;
encodedBody = encoded . body ;
2019-06-27 02:01:36 +05:30
}
2021-02-12 23:05:33 +05:30
2019-12-23 18:58:27 +05:30
const requestResult = this . _requestFn ( url , {
2019-06-27 02:01:36 +05:30
method ,
headers ,
2020-11-11 15:15:23 +05:30
body : encodedBody ,
2020-10-23 20:48:11 +05:30
timeout : options ? . timeout ,
2020-11-16 15:15:46 +05:30
uploadProgress : options ? . uploadProgress ,
2020-11-11 15:15:23 +05:30
format : "json" // response format
2019-06-27 02:01:36 +05:30
} ) ;
2020-04-05 18:41:15 +05:30
2021-02-12 23:05:33 +05:30
const wrapper = new RequestWrapper ( method , url , requestResult , log ) ;
2020-04-05 18:41:15 +05:30
if ( this . _reconnector ) {
wrapper . response ( ) . catch ( err => {
2020-09-25 14:15:41 +05:30
// Some endpoints such as /sync legitimately time-out
// (which is also reported as a ConnectionError) and will re-attempt,
// but spinning up the reconnector in this case is ok,
// as all code ran on session and sync start should be reentrant
2020-05-06 23:08:33 +05:30
if ( err . name === "ConnectionError" ) {
2020-04-05 18:41:15 +05:30
this . _reconnector . onRequestFailed ( this ) ;
}
} ) ;
}
return wrapper ;
2019-06-27 02:01:36 +05:30
}
2019-02-05 03:56:24 +05:30
2020-09-18 21:43:20 +05:30
_unauthedRequest ( method , url , queryParams , body , options ) {
return this . _baseRequest ( method , url , queryParams , body , options , null ) ;
}
_authedRequest ( method , url , queryParams , body , options ) {
return this . _baseRequest ( method , url , queryParams , body , options , this . _accessToken ) ;
}
2020-04-05 18:41:15 +05:30
_post ( csPath , queryParams , body , options ) {
2020-09-18 21:43:20 +05:30
return this . _authedRequest ( "POST" , this . _url ( csPath ) , queryParams , body , options ) ;
2019-06-27 02:01:36 +05:30
}
2019-02-05 03:56:24 +05:30
2020-04-05 18:41:15 +05:30
_put ( csPath , queryParams , body , options ) {
2020-09-18 21:43:20 +05:30
return this . _authedRequest ( "PUT" , this . _url ( csPath ) , queryParams , body , options ) ;
2019-07-27 01:33:57 +05:30
}
2020-04-05 18:41:15 +05:30
_get ( csPath , queryParams , body , options ) {
2020-09-18 21:43:20 +05:30
return this . _authedRequest ( "GET" , this . _url ( csPath ) , queryParams , body , options ) ;
2019-06-27 02:01:36 +05:30
}
2018-12-21 19:05:24 +05:30
2020-04-05 18:41:15 +05:30
sync ( since , filter , timeout , options = null ) {
return this . _get ( "/sync" , { since , timeout , filter } , null , options ) ;
2019-06-27 02:01:36 +05:30
}
2019-02-05 03:56:24 +05:30
2019-03-09 05:11:06 +05:30
// params is from, dir and optionally to, limit, filter.
2020-04-05 18:41:15 +05:30
messages ( roomId , params , options = null ) {
return this . _get ( ` /rooms/ ${ encodeURIComponent ( roomId ) } /messages ` , params , null , options ) ;
2019-03-09 05:11:06 +05:30
}
2020-08-19 19:41:33 +05:30
// params is at, membership and not_membership
members ( roomId , params , options = null ) {
return this . _get ( ` /rooms/ ${ encodeURIComponent ( roomId ) } /members ` , params , null , options ) ;
}
2020-04-05 18:41:15 +05:30
send ( roomId , eventType , txnId , content , options = null ) {
return this . _put ( ` /rooms/ ${ encodeURIComponent ( roomId ) } /send/ ${ encodeURIComponent ( eventType ) } / ${ encodeURIComponent ( txnId ) } ` , { } , content , options ) ;
2019-07-27 01:33:57 +05:30
}
2020-08-21 18:46:57 +05:30
receipt ( roomId , receiptType , eventId , options = null ) {
return this . _post ( ` /rooms/ ${ encodeURIComponent ( roomId ) } /receipt/ ${ encodeURIComponent ( receiptType ) } / ${ encodeURIComponent ( eventId ) } ` ,
{ } , { } , options ) ;
}
2020-09-08 14:23:15 +05:30
passwordLogin ( username , password , initialDeviceDisplayName , options = null ) {
2020-09-18 21:43:20 +05:30
return this . _unauthedRequest ( "POST" , this . _url ( "/login" ) , null , {
2019-02-05 03:56:24 +05:30
"type" : "m.login.password" ,
"identifier" : {
"type" : "m.id.user" ,
"user" : username
} ,
2020-09-08 14:23:15 +05:30
"password" : password ,
"initial_device_display_name" : initialDeviceDisplayName
2020-04-05 18:41:15 +05:30
} , options ) ;
2019-06-27 02:01:36 +05:30
}
2019-10-12 23:54:09 +05:30
2020-04-05 18:41:15 +05:30
createFilter ( userId , filter , options = null ) {
return this . _post ( ` /user/ ${ encodeURIComponent ( userId ) } /filter ` , null , filter , options ) ;
2019-10-12 23:54:09 +05:30
}
2020-03-31 03:26:03 +05:30
2020-04-05 18:41:15 +05:30
versions ( options = null ) {
2020-09-18 21:43:20 +05:30
return this . _unauthedRequest ( "GET" , ` ${ this . _homeserver } /_matrix/client/versions ` , null , null , options ) ;
2020-03-31 03:26:03 +05:30
}
2020-05-09 23:32:08 +05:30
2020-08-27 22:43:24 +05:30
uploadKeys ( payload , options = null ) {
return this . _post ( "/keys/upload" , null , payload , options ) ;
}
2020-08-31 17:54:09 +05:30
queryKeys ( queryRequest , options = null ) {
return this . _post ( "/keys/query" , null , queryRequest , options ) ;
}
2020-09-03 19:03:23 +05:30
claimKeys ( payload , options = null ) {
return this . _post ( "/keys/claim" , null , payload , options ) ;
}
2020-09-03 19:06:17 +05:30
sendToDevice ( type , payload , txnId , options = null ) {
return this . _put ( ` /sendToDevice/ ${ encodeURIComponent ( type ) } / ${ encodeURIComponent ( txnId ) } ` , null , payload , options ) ;
}
2020-09-17 17:49:57 +05:30
roomKeysVersion ( version = null , options = null ) {
2020-09-17 21:27:12 +05:30
let versionPart = "" ;
if ( version ) {
versionPart = ` / ${ encodeURIComponent ( version ) } ` ;
2020-09-17 17:49:57 +05:30
}
2020-09-17 21:27:12 +05:30
return this . _get ( ` /room_keys/version ${ versionPart } ` , null , null , options ) ;
2020-09-17 17:49:57 +05:30
}
roomKeyForRoomAndSession ( version , roomId , sessionId , options = null ) {
return this . _get ( ` /room_keys/keys/ ${ encodeURIComponent ( roomId ) } / ${ encodeURIComponent ( sessionId ) } ` , { version } , null , options ) ;
}
2020-11-11 15:15:44 +05:30
uploadAttachment ( blob , filename , options = null ) {
2020-11-11 16:20:40 +05:30
return this . _authedRequest ( "POST" , ` ${ this . _homeserver } /_matrix/media/r0/upload ` , { filename } , blob , options ) ;
2020-11-11 15:15:44 +05:30
}
2021-03-19 01:12:46 +05:30
setPusher ( pusher , options = null ) {
return this . _post ( "/pushers/set" , null , pusher , options ) ;
}
2021-04-01 18:29:46 +05:30
getPushers ( options = null ) {
return this . _get ( "/pushers" , null , null , options ) ;
}
2019-03-08 16:56:59 +05:30
}
2020-04-23 00:17:31 +05:30
export function tests ( ) {
function createRequestMock ( result ) {
return function ( ) {
return {
abort ( ) { } ,
response ( ) {
return Promise . resolve ( result ) ;
}
}
}
}
return {
"superficial happy path for GET" : async assert => {
const hsApi = new HomeServerApi ( {
request : createRequestMock ( { body : 42 , status : 200 } ) ,
homeServer : "https://hs.tld"
} ) ;
const result = await hsApi . _get ( "foo" , null , null , null ) . response ( ) ;
assert . strictEqual ( result , 42 ) ;
}
}
}