"use strict" ;
const Promise = require ( "bluebird" ) ;
const axios = require ( "axios" ) ;
const createError = require ( "create-error" ) ;
const url = require ( "url" ) ;
const asExpression = require ( "as-expression" ) ;
const dotty = require ( "dotty" ) ;
const getJson = require ( "axios-get-json-response" ) ;
const { ValidationError , validateValue , validateArguments , required , isString , arrayOf , allowExtraProperties } = require ( "validatem" ) ;
let LookupFailed = createError ( "LookupFailed" ) ;
let MethodNotAvailable = createError ( "MethodNotAvailable" ) ;
let manualAxios = axios . create ( getJson . axiosConfiguration ) ;
function generateBaseUrl ( host ) {
return url . format ( {
protocol : "https" ,
slashes : true ,
host : host ,
path : null
} ) ;
}
function throwLookupError ( reason ) {
throw new LookupFailed ( ` Could not autodiscover Matrix configuration; ${ reason } ` , { reason : reason } ) ;
}
/* TODO: Turn into stand-alone validateUrl(url, { httpsOnly }) module */
function validateUrl ( url _ ) {
let parsed = asExpression ( ( ) => {
try {
return url . parse ( url _ ) ;
} catch ( error ) {
if ( error instanceof URIError || error instanceof TypeError ) {
return false ;
} else {
throw error ;
}
}
} ) ;
return ( parsed . protocol === "https:" && parsed . slashes === true && parsed . host != null ) ;
}
/* TODO: Turn into stand-alone getSupportedVersions module (+ an API for checking whether a *specific* API is supported, semver-style) */
function validateHomeserverUrl ( homeserverUrl ) {
return Promise . try ( ( ) => {
return manualAxios . get ( url . resolve ( homeserverUrl , "/_matrix/client/versions" ) ) ;
} ) . then ( ( response ) => {
let json = getJson . parse ( response ) ;
validateValue ( json , allowExtraProperties ( {
versions : [ required , arrayOf ( [ required , isString ] ) ]
} ) ) ;
} ) . catch ( getJson . BadStatusCode , ( error ) => {
throwLookupError ( ` homeserver returned a ${ error . statusCode } status code, maybe it is not a Matrix server? ` ) ;
} ) . catch ( getJson . ParsingFailed , ( _error ) => {
throwLookupError ( "homeserver returned invalid JSON, maybe it is not a Matrix server?" ) ;
} ) . catch ( ValidationError , ( _error ) => {
throwLookupError ( "homeserver returned an invalid version response, maybe it is not a Matrix server?" ) ;
} ) . catch ( { code : "ENOTFOUND" } , ( ) => {
throwLookupError ( "hostname of homeserver does not exist" ) ;
} ) ;
}
/* TODO: Turn into stand-alone checkIdentityServer module */
function validateIdentityServerUrl ( identityServerUrl ) {
return Promise . try ( ( ) => {
return manualAxios . get ( url . resolve ( identityServerUrl , "/_matrix/identity/api/v1" ) ) ;
} ) . then ( ( response ) => {
/* We only care about the validation here, not the response data */
getJson . parse ( response ) ;
} ) . catch ( getJson . BadStatusCode , ( error ) => {
throwLookupError ( ` identity server returned a ${ error . statusCode } status code, maybe it is not really an identity server? ` ) ;
} ) . catch ( getJson . ParsingFailed , ( _error ) => {
throwLookupError ( "identity server returned invalid JSON, maybe it is not really an identity server?" ) ;
} ) ;
}
function attemptLiteralHostname ( host ) {
return Promise . try ( ( ) => {
let baseUrl = generateBaseUrl ( host ) ;
return Promise . try ( ( ) => {
/* TODO: Eventually, we may need to distinguish between a 404 and some other status code. For now, we assume that since this is the last-ditch option, any failure is terminal. */
return validateHomeserverUrl ( baseUrl ) ;
} ) . then ( ( ) => {
return {
method : "direct" ,
homeserver : baseUrl
} ;
} ) ;
} ) ;
}
function attemptWellKnown ( host ) {
return Promise . try ( ( ) => {
let baseUrl = generateBaseUrl ( host ) ;
return manualAxios . get ( url . resolve ( baseUrl , "/.well-known/matrix/client" ) ) ;
} ) . then ( ( response ) => {
let json = getJson . parse ( response ) ;
let homeserverUrl = asExpression ( ( ) => {
if ( ! dotty . exists ( json , [ "m.homeserver" , "base_url" ] ) ) {
throwLookupError ( "no homeserver specified in autodiscovered configuration" ) ;
} else {
let serverUrl = json [ "m.homeserver" ] . base _url ;
if ( ! validateUrl ( serverUrl ) ) {
throwLookupError ( "homeserver URL in autodiscovered configuration is not a valid HTTPS URL" ) ;
} else {
return serverUrl ;
}
}
} ) ;
let identityServerUrl = asExpression ( ( ) => {
if ( json [ "m.identity_server" ] != null ) {
let serverUrl = json [ "m.identity_server" ] . base _url ;
if ( serverUrl == null ) {
throwLookupError ( "autodiscovered configuration is invalid, and contains an empty identity_server object" ) ;
} else if ( ! validateUrl ( serverUrl ) ) {
throwLookupError ( "identity server URL in autodiscovered configuration is not a valid HTTPS URL" ) ;
} else {
return serverUrl ;
}
}
} ) ;
return Promise . all ( [
validateHomeserverUrl ( homeserverUrl ) ,
( identityServerUrl != null )
? validateIdentityServerUrl ( identityServerUrl )
: null
] ) . then ( ( ) => {
return {
method : "wellKnown" ,
homeserver : homeserverUrl ,
identityServer : identityServerUrl ,
raw : json
} ;
} ) ;
} ) . catch ( getJson . BadStatusCode , ( error ) => {
if ( error . statusCode === 404 ) {
throw new MethodNotAvailable ( ".well-known URL returned a 404" ) ;
} else {
throwLookupError ( ` host returned a ${ error . statusCode } status code ` ) ;
}
} ) . catch ( getJson . ParsingFailed , ( _error ) => {
throwLookupError ( "host returned invalid JSON" ) ;
} ) . catch ( { code : "ENOTFOUND" } , ( ) => {
throwLookupError ( "hostname does not exist" ) ;
} ) ;
}
module . exports = {
LookupFailed : LookupFailed ,
discover : function ( hostname ) {
validateArguments ( arguments , [
[ "hostname" , required , isString ]
] ) ;
return Promise . try ( ( ) => {
return attemptWellKnown ( hostname ) ;
} ) . catch ( MethodNotAvailable , ( ) => {
return attemptLiteralHostname ( hostname ) ;
} ) . catch ( MethodNotAvailable , ( ) => {
throwLookupError ( "none of the autodiscovery methods were available" ) ;
} ) ;
}
} ;