| 1 |
<?php |
| 2 |
/** |
| 3 |
* OpenStation — Network identity: what one install tells another about |
| 4 |
* itself, and the request layer the two sides share. |
| 5 |
* |
| 6 |
* `GET /desktop-mode/v1/network/identity` is public: a site's name, |
| 7 |
* its URL, its shell screen and its public key are what a hub pins |
| 8 |
* when pairing and what a member pins about its hub. Nothing secret is |
| 9 |
* in it, and nothing in it is trusted until it has been pinned. |
| 10 |
* |
| 11 |
* @package OpenStation |
| 12 |
*/ |
| 13 |
|
| 14 |
defined( 'ABSPATH' ) || exit; |
| 15 |
|
| 16 |
/** |
| 17 |
* This install's identity: the facts another install pins about it. |
| 18 |
* |
| 19 |
* @return array{url:string,name:string,shellUrl:string,publicKey:string,multisite:bool} |
| 20 |
*/ |
| 21 |
function openstation_network_identity() { |
| 22 |
$screen = 'admin.php?page=' . OPENSTATION_SHELL_PAGE_SLUG; |
| 23 |
if ( is_multisite() ) { |
| 24 |
$network = get_network(); |
| 25 |
return array( |
| 26 |
'url' => esc_url_raw( network_home_url( '/' ) ), |
| 27 |
'name' => (string) ( $network ? $network->site_name : get_bloginfo( 'name' ) ), |
| 28 |
'shellUrl' => esc_url_raw( network_admin_url( $screen ) ), |
| 29 |
'publicKey' => openstation_network_public_key(), |
| 30 |
'multisite' => true, |
| 31 |
); |
| 32 |
} |
| 33 |
return array( |
| 34 |
'url' => esc_url_raw( home_url( '/' ) ), |
| 35 |
'name' => (string) get_bloginfo( 'name' ), |
| 36 |
'shellUrl' => esc_url_raw( admin_url( $screen ) ), |
| 37 |
'publicKey' => openstation_network_public_key(), |
| 38 |
'multisite' => false, |
| 39 |
); |
| 40 |
} |
| 41 |
|
| 42 |
/** |
| 43 |
* Whether a remote install may be reached over this URL: HTTPS, unless |
| 44 |
* the install itself runs in a local or development environment, where |
| 45 |
* plain HTTP between two containers is the whole point. |
| 46 |
* |
| 47 |
* @param string $url URL. |
| 48 |
* @return bool |
| 49 |
*/ |
| 50 |
function openstation_network_url_allowed( $url ) { |
| 51 |
$scheme = wp_parse_url( (string) $url, PHP_URL_SCHEME ); |
| 52 |
if ( 'https' === $scheme ) { |
| 53 |
return true; |
| 54 |
} |
| 55 |
return 'http' === $scheme && in_array( wp_get_environment_type(), array( 'local', 'development' ), true ); |
| 56 |
} |
| 57 |
|
| 58 |
/** |
| 59 |
* A GET to another install's REST route, with the URL it is actually |
| 60 |
* reached by. |
| 61 |
* |
| 62 |
* @param string $base The other install's URL (its home). |
| 63 |
* @param string $route REST route, `/desktop-mode/v1/network/identity`. |
| 64 |
* @param array<string,string> $headers Extra headers (a signature). |
| 65 |
* @return array|WP_Error Decoded JSON body, or the error. |
| 66 |
*/ |
| 67 |
function openstation_network_remote_get( $base, $route, array $headers = array() ) { |
| 68 |
$base = untrailingslashit( (string) $base ); |
| 69 |
if ( ! openstation_network_url_allowed( $base ) ) { |
| 70 |
return new WP_Error( 'openstation_network_insecure', __( 'Sites in a network talk over HTTPS.', 'desktop-mode' ) ); |
| 71 |
} |
| 72 |
$url = $base . '/wp-json' . $route; |
| 73 |
|
| 74 |
/** |
| 75 |
* Filters the URL one install reaches another by. |
| 76 |
* |
| 77 |
* The address a site is known by is not always the address its |
| 78 |
* server can be reached at: an internal hostname behind a proxy, a |
| 79 |
* container beside another container. The identity and the pinned |
| 80 |
* key stay keyed by the public URL; only the wire address changes. |
| 81 |
* |
| 82 |
* @param string $url The URL about to be requested. |
| 83 |
* @param string $base The install's public URL. |
| 84 |
*/ |
| 85 |
$url = (string) apply_filters( 'openstation_network_request_url', $url, $base ); |
| 86 |
|
| 87 |
$response = wp_remote_get( |
| 88 |
$url, |
| 89 |
array( |
| 90 |
'timeout' => 8, |
| 91 |
// An identity or list request never moves; a redirect is a |
| 92 |
// misconfigured host (a multisite sending an unknown Host to |
| 93 |
// signup) and is reported as such rather than followed. |
| 94 |
'redirection' => 0, |
| 95 |
'headers' => array_merge( array( 'Accept' => 'application/json' ), $headers ), |
| 96 |
) |
| 97 |
); |
| 98 |
if ( is_wp_error( $response ) ) { |
| 99 |
return $response; |
| 100 |
} |
| 101 |
$code = (int) wp_remote_retrieve_response_code( $response ); |
| 102 |
$body = json_decode( (string) wp_remote_retrieve_body( $response ), true ); |
| 103 |
if ( 200 !== $code || ! is_array( $body ) ) { |
| 104 |
$message = is_array( $body ) && ! empty( $body['message'] ) ? (string) $body['message'] : ''; |
| 105 |
return new WP_Error( |
| 106 |
'openstation_network_http_' . $code, |
| 107 |
'' !== $message |
| 108 |
? $message |
| 109 |
/* translators: %d: HTTP status code. */ |
| 110 |
: sprintf( __( 'The site answered with HTTP %d.', 'desktop-mode' ), $code ) |
| 111 |
); |
| 112 |
} |
| 113 |
return $body; |
| 114 |
} |
| 115 |
|
| 116 |
/** |
| 117 |
* Fetch and validate another install's identity. |
| 118 |
* |
| 119 |
* @param string $url The install's URL. |
| 120 |
* @return array|WP_Error Identity, or the error. |
| 121 |
*/ |
| 122 |
function openstation_network_fetch_identity( $url ) { |
| 123 |
$identity = openstation_network_remote_get( $url, '/desktop-mode/v1/network/identity' ); |
| 124 |
if ( is_wp_error( $identity ) ) { |
| 125 |
return $identity; |
| 126 |
} |
| 127 |
foreach ( array( 'url', 'name', 'shellUrl', 'publicKey' ) as $key ) { |
| 128 |
if ( empty( $identity[ $key ] ) || ! is_string( $identity[ $key ] ) ) { |
| 129 |
return new WP_Error( 'openstation_network_no_identity', __( 'That site does not run OpenStation, or its network identity is unreachable.', 'desktop-mode' ) ); |
| 130 |
} |
| 131 |
} |
| 132 |
if ( ! openstation_network_is_public_key( $identity['publicKey'] ) ) { |
| 133 |
return new WP_Error( 'openstation_network_bad_key', __( 'That site published a public key OpenStation cannot use.', 'desktop-mode' ) ); |
| 134 |
} |
| 135 |
return array( |
| 136 |
'url' => esc_url_raw( $identity['url'] ), |
| 137 |
'name' => sanitize_text_field( $identity['name'] ), |
| 138 |
'shellUrl' => esc_url_raw( $identity['shellUrl'] ), |
| 139 |
'publicKey' => $identity['publicKey'], |
| 140 |
'multisite' => ! empty( $identity['multisite'] ), |
| 141 |
); |
| 142 |
} |
| 143 |
|
| 144 |
/** |
| 145 |
* Register the identity route. |
| 146 |
*/ |
| 147 |
function openstation_network_register_identity_route() { |
| 148 |
register_rest_route( |
| 149 |
'desktop-mode/v1', |
| 150 |
'/network/identity', |
| 151 |
array( |
| 152 |
'methods' => WP_REST_Server::READABLE, |
| 153 |
'callback' => 'openstation_rest_network_identity', |
| 154 |
'permission_callback' => '__return_true', |
| 155 |
) |
| 156 |
); |
| 157 |
} |
| 158 |
add_action( 'rest_api_init', 'openstation_network_register_identity_route' ); |
| 159 |
|
| 160 |
/** |
| 161 |
* GET /desktop-mode/v1/network/identity |
| 162 |
* |
| 163 |
* @return WP_REST_Response |
| 164 |
*/ |
| 165 |
function openstation_rest_network_identity() { |
| 166 |
return rest_ensure_response( openstation_network_identity() ); |
| 167 |
} |
| 168 |
|