2020-08-05 18:38:55 +02:00
/ *
Copyright 2020 Bruno Windels < bruno @ windels . cloud >
2020-09-22 13:40:38 +02:00
Copyright 2020 The Matrix . org Foundation C . I . C .
2020-08-05 18:38:55 +02:00
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-05 21:24:14 +01:00
import { HomeServerError , ConnectionError } from "../error.js" ;
2020-09-22 13:40:38 +02:00
import { encodeQueryParams } from "./common.js" ;
2019-02-10 21:25:29 +01:00
2019-02-04 23:26:24 +01:00
class RequestWrapper {
2020-08-05 17:36:44 +02:00
constructor ( method , url , requestResult ) {
2019-12-23 14:28:27 +01:00
this . _requestResult = requestResult ;
2020-08-05 17:36:44 +02:00
this . _promise = requestResult . response ( ) . then ( response => {
2019-12-23 14:28:27 +01:00
// ok?
if ( response . status >= 200 && response . status < 300 ) {
return response . body ;
} else {
2020-11-05 21:24:14 +01:00
if ( response . status >= 400 && ! response . body ? . errcode ) {
throw new ConnectionError ( ` HTTP error status ${ response . status } without errcode in body, assume this is a load balancer complaining the server is offline. ` ) ;
} else {
throw new HomeServerError ( method , url , response . body , response . status ) ;
2019-12-23 14:28:27 +01:00
}
}
2020-11-05 22:39:32 +01:00
} , err => {
// if this._requestResult is still set, the abort error came not from calling abort here
if ( err . name === "AbortError" && this . _requestResult ) {
throw new Error ( ` Request ${ method } ${ url } was unexpectedly aborted, see #187. ` ) ;
} else {
throw err ;
}
2019-12-23 14:28:27 +01:00
} ) ;
2019-06-26 22:31:36 +02:00
}
2018-12-21 14:35:24 +01:00
2019-06-26 22:31:36 +02:00
abort ( ) {
2020-11-05 22:39:32 +01:00
if ( this . _requestResult ) {
this . _requestResult . abort ( ) ;
// to mark that it was on purpose in above rejection handler
this . _requestResult = null ;
}
2019-06-26 22:31:36 +02:00
}
2018-12-21 14:35:24 +01:00
2019-06-26 22:31:36 +02:00
response ( ) {
return this . _promise ;
}
2018-12-21 14:35:24 +01:00
}
2020-11-11 10:45:23 +01:00
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-20 21:26:39 +02:00
export class HomeServerApi {
2020-04-05 15:11:15 +02:00
constructor ( { homeServer , accessToken , request , createTimeout , reconnector } ) {
2019-03-08 20:03:47 +01:00
// 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 14:28:27 +01:00
this . _homeserver = homeServer ;
2019-06-26 22:31:36 +02:00
this . _accessToken = accessToken ;
2019-12-23 14:28:27 +01:00
this . _requestFn = request ;
2020-04-05 15:11:15 +02:00
this . _createTimeout = createTimeout ;
this . _reconnector = reconnector ;
2019-06-26 22:31:36 +02:00
}
2018-12-21 14:35:24 +01:00
2019-06-26 22:31:36 +02:00
_url ( csPath ) {
return ` ${ this . _homeserver } /_matrix/client/r0 ${ csPath } ` ;
}
2018-12-21 14:35:24 +01:00
2020-09-18 18:13:20 +02:00
_baseRequest ( method , url , queryParams , body , options , accessToken ) {
2020-08-20 15:40:43 +02:00
const queryString = encodeQueryParams ( queryParams ) ;
2020-03-30 23:56:03 +02:00
url = ` ${ url } ? ${ queryString } ` ;
2020-11-11 10:45:23 +01:00
let encodedBody ;
2020-04-22 20:46:47 +02:00
const headers = new Map ( ) ;
2020-09-18 18:13:20 +02:00
if ( accessToken ) {
headers . set ( "Authorization" , ` Bearer ${ accessToken } ` ) ;
2019-06-26 22:31:36 +02:00
}
2020-04-22 20:46:47 +02:00
headers . set ( "Accept" , "application/json" ) ;
2019-06-26 22:31:36 +02:00
if ( body ) {
2020-11-11 10:45:23 +01:00
const encoded = encodeBody ( body ) ;
headers . set ( "Content-Type" , encoded . mimeType ) ;
headers . set ( "Content-Length" , encoded . length ) ;
encodedBody = encoded . body ;
2019-06-26 22:31:36 +02:00
}
2019-12-23 14:28:27 +01:00
const requestResult = this . _requestFn ( url , {
2019-06-26 22:31:36 +02:00
method ,
headers ,
2020-11-11 10:45:23 +01:00
body : encodedBody ,
2020-10-23 17:18:11 +02:00
timeout : options ? . timeout ,
2020-11-16 10:45:46 +01:00
uploadProgress : options ? . uploadProgress ,
2020-11-11 10:45:23 +01:00
format : "json" // response format
2019-06-26 22:31:36 +02:00
} ) ;
2020-04-05 15:11:15 +02:00
2020-08-05 17:36:44 +02:00
const wrapper = new RequestWrapper ( method , url , requestResult ) ;
2020-04-05 15:11:15 +02:00
if ( this . _reconnector ) {
wrapper . response ( ) . catch ( err => {
2020-09-25 10:45:41 +02:00
// 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 19:38:33 +02:00
if ( err . name === "ConnectionError" ) {
2020-04-05 15:11:15 +02:00
this . _reconnector . onRequestFailed ( this ) ;
}
} ) ;
}
return wrapper ;
2019-06-26 22:31:36 +02:00
}
2019-02-04 23:26:24 +01:00
2020-09-18 18:13:20 +02:00
_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 15:11:15 +02:00
_post ( csPath , queryParams , body , options ) {
2020-09-18 18:13:20 +02:00
return this . _authedRequest ( "POST" , this . _url ( csPath ) , queryParams , body , options ) ;
2019-06-26 22:31:36 +02:00
}
2019-02-04 23:26:24 +01:00
2020-04-05 15:11:15 +02:00
_put ( csPath , queryParams , body , options ) {
2020-09-18 18:13:20 +02:00
return this . _authedRequest ( "PUT" , this . _url ( csPath ) , queryParams , body , options ) ;
2019-07-26 22:03:57 +02:00
}
2020-04-05 15:11:15 +02:00
_get ( csPath , queryParams , body , options ) {
2020-09-18 18:13:20 +02:00
return this . _authedRequest ( "GET" , this . _url ( csPath ) , queryParams , body , options ) ;
2019-06-26 22:31:36 +02:00
}
2018-12-21 14:35:24 +01:00
2020-04-05 15:11:15 +02:00
sync ( since , filter , timeout , options = null ) {
return this . _get ( "/sync" , { since , timeout , filter } , null , options ) ;
2019-06-26 22:31:36 +02:00
}
2019-02-04 23:26:24 +01:00
2019-03-09 00:41:06 +01:00
// params is from, dir and optionally to, limit, filter.
2020-04-05 15:11:15 +02:00
messages ( roomId , params , options = null ) {
return this . _get ( ` /rooms/ ${ encodeURIComponent ( roomId ) } /messages ` , params , null , options ) ;
2019-03-09 00:41:06 +01:00
}
2020-08-19 16:11:33 +02:00
// 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 15:11:15 +02:00
send ( roomId , eventType , txnId , content , options = null ) {
return this . _put ( ` /rooms/ ${ encodeURIComponent ( roomId ) } /send/ ${ encodeURIComponent ( eventType ) } / ${ encodeURIComponent ( txnId ) } ` , { } , content , options ) ;
2019-07-26 22:03:57 +02:00
}
2020-08-21 15:16:57 +02:00
receipt ( roomId , receiptType , eventId , options = null ) {
return this . _post ( ` /rooms/ ${ encodeURIComponent ( roomId ) } /receipt/ ${ encodeURIComponent ( receiptType ) } / ${ encodeURIComponent ( eventId ) } ` ,
{ } , { } , options ) ;
}
2020-09-08 10:53:15 +02:00
passwordLogin ( username , password , initialDeviceDisplayName , options = null ) {
2020-09-18 18:13:20 +02:00
return this . _unauthedRequest ( "POST" , this . _url ( "/login" ) , null , {
2019-02-04 23:26:24 +01:00
"type" : "m.login.password" ,
"identifier" : {
"type" : "m.id.user" ,
"user" : username
} ,
2020-09-08 10:53:15 +02:00
"password" : password ,
"initial_device_display_name" : initialDeviceDisplayName
2020-04-05 15:11:15 +02:00
} , options ) ;
2019-06-26 22:31:36 +02:00
}
2019-10-12 20:24:09 +02:00
2020-04-05 15:11:15 +02:00
createFilter ( userId , filter , options = null ) {
return this . _post ( ` /user/ ${ encodeURIComponent ( userId ) } /filter ` , null , filter , options ) ;
2019-10-12 20:24:09 +02:00
}
2020-03-30 23:56:03 +02:00
2020-04-05 15:11:15 +02:00
versions ( options = null ) {
2020-09-18 18:13:20 +02:00
return this . _unauthedRequest ( "GET" , ` ${ this . _homeserver } /_matrix/client/versions ` , null , null , options ) ;
2020-03-30 23:56:03 +02:00
}
2020-05-09 20:02:08 +02:00
2020-08-27 19:13:24 +02:00
uploadKeys ( payload , options = null ) {
return this . _post ( "/keys/upload" , null , payload , options ) ;
}
2020-08-31 14:24:09 +02:00
queryKeys ( queryRequest , options = null ) {
return this . _post ( "/keys/query" , null , queryRequest , options ) ;
}
2020-09-03 15:33:23 +02:00
claimKeys ( payload , options = null ) {
return this . _post ( "/keys/claim" , null , payload , options ) ;
}
2020-09-03 15:36:17 +02:00
sendToDevice ( type , payload , txnId , options = null ) {
return this . _put ( ` /sendToDevice/ ${ encodeURIComponent ( type ) } / ${ encodeURIComponent ( txnId ) } ` , null , payload , options ) ;
}
2020-09-17 14:19:57 +02:00
roomKeysVersion ( version = null , options = null ) {
2020-09-17 17:57:12 +02:00
let versionPart = "" ;
if ( version ) {
versionPart = ` / ${ encodeURIComponent ( version ) } ` ;
2020-09-17 14:19:57 +02:00
}
2020-09-17 17:57:12 +02:00
return this . _get ( ` /room_keys/version ${ versionPart } ` , null , null , options ) ;
2020-09-17 14:19:57 +02:00
}
roomKeyForRoomAndSession ( version , roomId , sessionId , options = null ) {
return this . _get ( ` /room_keys/keys/ ${ encodeURIComponent ( roomId ) } / ${ encodeURIComponent ( sessionId ) } ` , { version } , null , options ) ;
}
2020-11-11 10:45:44 +01:00
uploadAttachment ( blob , filename , options = null ) {
2020-11-11 11:50:40 +01:00
return this . _authedRequest ( "POST" , ` ${ this . _homeserver } /_matrix/media/r0/upload ` , { filename } , blob , options ) ;
2020-11-11 10:45:44 +01:00
}
2019-03-08 12:26:59 +01:00
}
2020-04-22 20:47:31 +02:00
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 ) ;
}
}
}