mirror of
https://github.com/OutlineFoundation/outline-server.git
synced 2026-08-04 14:37:34 +00:00
Allow users to change the hostname for new access keys (#565)
* Create an endpoint to change the server's hostname. This is the backend only, and does not check if the hostname exists or points to a Shadowbox server. Users have the responsibility of double checking that the hostname and DNS if necessary are properly set up and specified. * Updates Changes validation to use regex. Gets rid of a bad test Uses paramtereized tests for better test output Adds setHostname to AccessKeyRepository and calls it in the endpoint * Integration tests Also fixes missing dependency on ip-regex * Remove validator from lockfile * Ensure we correctly use Python 2 for Cloud SDK * API for the new endpoint to change the server hostname. (#534) * API for the new endpoint to change the server hostname. I am sending it up now so we can iterate on it while I'm busy implementing the endpoint. I will merge it into the feature branch when it's done as opposed as into master. * API for the new endpoint to change the server hostname. I am sending it up now so we can iterate on it while I'm busy implementing the endpoint. I will merge it into the feature branch when it's done as opposed as into master. * Remove 404 and 406 error states. Checking for a functioning Shadowbox server requires knowledge of the API prefix, which creates layering challenges. The UI will be able to make these checks, but users who call the API directly will have to be careful not to put in an incorrect hostname. The consequence of this would be that newly made access keys won't work. * Clarify API, change name, add validation errors * Clarify description * Remove the API response corresponding to a failed check against the new hostname that it points to the same server * Respond to review comments * Rename endpoint and related functions From the server's perspective, this does change the hostname for existing access keys since on server load time (in the Server Manager) we recalculate access key hostnames from the key repository. * Respond to review comments * Use version 1.2.0 since data limits is taking 1.1.0 * Fix update_mmdb and the Docker image to work on Alpine (#559) * Fix the for loop in update_mmdb to be `dash` compatible for x in {a..b} is bash-only, and alpine's default shell is `dash`. * Fix other issues related to using musl/Busybox/dash * Use `[[` instead of `((` for the branch * Make sure to update to GNU coreutils in the image in order to use `date --date` * Respond to review comments * UI for setting the hostname for access key invitations (#553) * Increase Shadowbox version for data limits experiment (#544) * Return the port for new access keys from GET /server. (#552) This was accidentally removed in #537. * UI for setting the hostname for access key invitations This also includes some random typo fixes and QOL improvements. To test, you can use export SB_IMAGE=cohenjongoogle/shadowbox:latest yarn do shadowbox/docker/run then in a different terminal yarn do server_manager/electron_app/run and add the server * Add back in new files Also fixes a backward compatibility issues for servers which don't support changing the hostname * Respond to review comments * Respond to review comments * Respond to review comments * Respond to review comments * Use DB-IP's Geolocation database instead of MaxMind's (#557) * Use DB-IP's free IP-to-Country database instead of MaxMind's This avoids the necesseity to either (1) bundle MaxMind's database in our docker image or (2) require server admins to individually create MaxMind accounts due to the new requirements for using GeoLite2 databases. See https://blog.maxmind.com/2019/12/18/significant-changes-to-accessing-and-using-geolite2-databases/ * Add attribution to DB-IP for the GeoIP database * Make sure that we deal with lag between the changeover of the current month and release of new versions of the DB-IP database * Respond to review comments * Attribute DB-IP in the update script * Respond to review comments * Respond to review comments * Fix update_mmdb and the Docker image to work on Alpine (#559) * Fix the for loop in update_mmdb to be `dash` compatible for x in {a..b} is bash-only, and alpine's default shell is `dash`. * Fix other issues related to using musl/Busybox/dash * Use `[[` instead of `((` for the branch * Make sure to update to GNU coreutils in the image in order to use `date --date` * Respond to review comments * Update to Typescript 3.7 (#558) See https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-7.html for more information. Among the nice new features are optional chainging and null coalescing, and assertion functions, as well as some additional safety checks in the compiler * Create an endpoint to change the server's hostname. This is the backend only, and does not check if the hostname exists or points to a Shadowbox server. Users have the responsibility of double checking that the hostname and DNS if necessary are properly set up and specified. * Updates Changes validation to use regex. Gets rid of a bad test Uses paramtereized tests for better test output Adds setHostname to AccessKeyRepository and calls it in the endpoint * Integration tests Also fixes missing dependency on ip-regex * Remove validator from lockfile * Ensure we correctly use Python 2 for Cloud SDK * API for the new endpoint to change the server hostname. (#534) * API for the new endpoint to change the server hostname. I am sending it up now so we can iterate on it while I'm busy implementing the endpoint. I will merge it into the feature branch when it's done as opposed as into master. * API for the new endpoint to change the server hostname. I am sending it up now so we can iterate on it while I'm busy implementing the endpoint. I will merge it into the feature branch when it's done as opposed as into master. * Remove 404 and 406 error states. Checking for a functioning Shadowbox server requires knowledge of the API prefix, which creates layering challenges. The UI will be able to make these checks, but users who call the API directly will have to be careful not to put in an incorrect hostname. The consequence of this would be that newly made access keys won't work. * Clarify API, change name, add validation errors * Clarify description * Remove the API response corresponding to a failed check against the new hostname that it points to the same server * Respond to review comments * Rename endpoint and related functions From the server's perspective, this does change the hostname for existing access keys since on server load time (in the Server Manager) we recalculate access key hostnames from the key repository. * Respond to review comments * Use version 1.2.0 since data limits is taking 1.1.0 * Fix update_mmdb and the Docker image to work on Alpine (#559) * Fix the for loop in update_mmdb to be `dash` compatible for x in {a..b} is bash-only, and alpine's default shell is `dash`. * Fix other issues related to using musl/Busybox/dash * Use `[[` instead of `((` for the branch * Make sure to update to GNU coreutils in the image in order to use `date --date` * Respond to review comments * UI for setting the hostname for access key invitations This also includes some random typo fixes and QOL improvements. To test, you can use export SB_IMAGE=cohenjongoogle/shadowbox:latest yarn do shadowbox/docker/run then in a different terminal yarn do server_manager/electron_app/run and add the server * Add back in new files Also fixes a backward compatibility issues for servers which don't support changing the hostname * Respond to review comments * Respond to review comments * Respond to review comments * Respond to review comments * Fix messed up rebase * merge * Clear merge markers Co-authored-by: alalamav <2132122+alalamav@users.noreply.github.com> * Create an endpoint to change the server's hostname. This is the backend only, and does not check if the hostname exists or points to a Shadowbox server. Users have the responsibility of double checking that the hostname and DNS if necessary are properly set up and specified. * Updates Changes validation to use regex. Gets rid of a bad test Uses paramtereized tests for better test output Adds setHostname to AccessKeyRepository and calls it in the endpoint * Integration tests Also fixes missing dependency on ip-regex * Remove validator from lockfile * Ensure we correctly use Python 2 for Cloud SDK * API for the new endpoint to change the server hostname. (#534) * API for the new endpoint to change the server hostname. I am sending it up now so we can iterate on it while I'm busy implementing the endpoint. I will merge it into the feature branch when it's done as opposed as into master. * API for the new endpoint to change the server hostname. I am sending it up now so we can iterate on it while I'm busy implementing the endpoint. I will merge it into the feature branch when it's done as opposed as into master. * Remove 404 and 406 error states. Checking for a functioning Shadowbox server requires knowledge of the API prefix, which creates layering challenges. The UI will be able to make these checks, but users who call the API directly will have to be careful not to put in an incorrect hostname. The consequence of this would be that newly made access keys won't work. * Clarify API, change name, add validation errors * Clarify description * Remove the API response corresponding to a failed check against the new hostname that it points to the same server * Respond to review comments * Rename endpoint and related functions From the server's perspective, this does change the hostname for existing access keys since on server load time (in the Server Manager) we recalculate access key hostnames from the key repository. * Respond to review comments * Use version 1.2.0 since data limits is taking 1.1.0 * UI for setting the hostname for access key invitations (#553) * Increase Shadowbox version for data limits experiment (#544) * Return the port for new access keys from GET /server. (#552) This was accidentally removed in #537. * UI for setting the hostname for access key invitations This also includes some random typo fixes and QOL improvements. To test, you can use export SB_IMAGE=cohenjongoogle/shadowbox:latest yarn do shadowbox/docker/run then in a different terminal yarn do server_manager/electron_app/run and add the server * Add back in new files Also fixes a backward compatibility issues for servers which don't support changing the hostname * Respond to review comments * Respond to review comments * Respond to review comments * Respond to review comments * Use DB-IP's Geolocation database instead of MaxMind's (#557) * Use DB-IP's free IP-to-Country database instead of MaxMind's This avoids the necesseity to either (1) bundle MaxMind's database in our docker image or (2) require server admins to individually create MaxMind accounts due to the new requirements for using GeoLite2 databases. See https://blog.maxmind.com/2019/12/18/significant-changes-to-accessing-and-using-geolite2-databases/ * Add attribution to DB-IP for the GeoIP database * Make sure that we deal with lag between the changeover of the current month and release of new versions of the DB-IP database * Respond to review comments * Attribute DB-IP in the update script * Respond to review comments * Respond to review comments * Fix update_mmdb and the Docker image to work on Alpine (#559) * Fix the for loop in update_mmdb to be `dash` compatible for x in {a..b} is bash-only, and alpine's default shell is `dash`. * Fix other issues related to using musl/Busybox/dash * Use `[[` instead of `((` for the branch * Make sure to update to GNU coreutils in the image in order to use `date --date` * Respond to review comments * Update to Typescript 3.7 (#558) See https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-7.html for more information. Among the nice new features are optional chainging and null coalescing, and assertion functions, as well as some additional safety checks in the compiler * Create an endpoint to change the server's hostname. This is the backend only, and does not check if the hostname exists or points to a Shadowbox server. Users have the responsibility of double checking that the hostname and DNS if necessary are properly set up and specified. * Updates Changes validation to use regex. Gets rid of a bad test Uses paramtereized tests for better test output Adds setHostname to AccessKeyRepository and calls it in the endpoint * Integration tests Also fixes missing dependency on ip-regex * Remove validator from lockfile * Ensure we correctly use Python 2 for Cloud SDK * API for the new endpoint to change the server hostname. (#534) * API for the new endpoint to change the server hostname. I am sending it up now so we can iterate on it while I'm busy implementing the endpoint. I will merge it into the feature branch when it's done as opposed as into master. * API for the new endpoint to change the server hostname. I am sending it up now so we can iterate on it while I'm busy implementing the endpoint. I will merge it into the feature branch when it's done as opposed as into master. * Remove 404 and 406 error states. Checking for a functioning Shadowbox server requires knowledge of the API prefix, which creates layering challenges. The UI will be able to make these checks, but users who call the API directly will have to be careful not to put in an incorrect hostname. The consequence of this would be that newly made access keys won't work. * Clarify API, change name, add validation errors * Clarify description * Remove the API response corresponding to a failed check against the new hostname that it points to the same server * Respond to review comments * Rename endpoint and related functions From the server's perspective, this does change the hostname for existing access keys since on server load time (in the Server Manager) we recalculate access key hostnames from the key repository. * Respond to review comments * Use version 1.2.0 since data limits is taking 1.1.0 * Fix update_mmdb and the Docker image to work on Alpine (#559) * Fix the for loop in update_mmdb to be `dash` compatible for x in {a..b} is bash-only, and alpine's default shell is `dash`. * Fix other issues related to using musl/Busybox/dash * Use `[[` instead of `((` for the branch * Make sure to update to GNU coreutils in the image in order to use `date --date` * Respond to review comments * UI for setting the hostname for access key invitations This also includes some random typo fixes and QOL improvements. To test, you can use export SB_IMAGE=cohenjongoogle/shadowbox:latest yarn do shadowbox/docker/run then in a different terminal yarn do server_manager/electron_app/run and add the server * Add back in new files Also fixes a backward compatibility issues for servers which don't support changing the hostname * Respond to review comments * Respond to review comments * Respond to review comments * Respond to review comments * Fix messed up rebase * merge * Clear merge markers Co-authored-by: alalamav <2132122+alalamav@users.noreply.github.com> Co-authored-by: alalamav <2132122+alalamav@users.noreply.github.com>
This commit is contained in:
parent
4b95dbd23f
commit
dafa1f5bc2
24 changed files with 590 additions and 195 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -4,3 +4,4 @@
|
|||
node_modules/
|
||||
/src/server_manager/install_scripts/do_install_script.ts
|
||||
yarn-error.log
|
||||
.vscode/
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@
|
|||
"error-do-auth": "Authentication with DigitalOcean failed",
|
||||
"error-do-regions": "Failed to get list of available regions",
|
||||
"error-feedback": "Failed to submit feedback. Please try again.",
|
||||
"error-hostname-invalid": "Must be an IP address or valid hostname.",
|
||||
"error-key-add": "Failed to add key",
|
||||
"error-key-remove": "Failed to remove key",
|
||||
"error-key-rename": "Failed to rename key",
|
||||
|
|
|
|||
|
|
@ -189,6 +189,10 @@
|
|||
"message": "Failed to submit feedback. Please try again.",
|
||||
"description": "This string appears in an error notification toast. It is shown when there is an error submitting the user's feedback."
|
||||
},
|
||||
"error_hostname_invalid": {
|
||||
"message": "Must be an IP address or valid hostname.",
|
||||
"description": "This string appears in an inline error message. It signifies that the user has input an invalid hostname."
|
||||
},
|
||||
"error_key_add": {
|
||||
"message": "Failed to add key",
|
||||
"description": "This string appears in an error notification toast. It is shown when there is an error creating a server access key."
|
||||
|
|
|
|||
|
|
@ -53,7 +53,10 @@ export interface Server {
|
|||
getCreatedDate(): Date;
|
||||
|
||||
// Returns the server's domain name or IP address.
|
||||
getHostname(): string;
|
||||
getHostnameForAccessKeys(): string;
|
||||
|
||||
// Changes the hostname for shared access keys.
|
||||
setHostnameForAccessKeys(hostname: string): Promise<void>;
|
||||
|
||||
// Returns the server's management API URL.
|
||||
getManagementApiUrl(): string;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,71 @@
|
|||
<!--
|
||||
Copyright 2020 The Outline Authors
|
||||
|
||||
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.
|
||||
-->
|
||||
|
||||
<!-- outline-server-settings-styles
|
||||
This file holds common styles for outline-server-settings and outline-validated-input
|
||||
-->
|
||||
|
||||
<dom-module id='outline-server-settings-styles'>
|
||||
<template>
|
||||
<style>
|
||||
/* Skip processing these with postcss-rtl as it incorrectly parses the border-color
|
||||
in the paper-input-container-underline-focus mixin.
|
||||
https://github.com/vkalinichev/postcss-rtl/issues/50 */
|
||||
/* rtl:begin:ignore */
|
||||
paper-input {
|
||||
/* Removes extra padding added by children of paper-input */
|
||||
margin-top: -8px;
|
||||
/* Create space for error messages */
|
||||
margin-bottom: 14px;
|
||||
--paper-input-container-label-focus: {
|
||||
color: var(--primary-green);
|
||||
};
|
||||
--paper-input-container-underline-focus: {
|
||||
border-color: var(--primary-green);
|
||||
};
|
||||
--paper-input-container-label: {
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
};
|
||||
--paper-input-container-color: var(--medium-gray);
|
||||
--paper-input-container-input: {
|
||||
color: #fff;
|
||||
};
|
||||
--paper-input-container-invalid-color: #f28b82;
|
||||
}
|
||||
/* rtl:end:ignore */
|
||||
|
||||
paper-input[readonly] {
|
||||
--paper-input-container-underline: {
|
||||
display: none;
|
||||
};
|
||||
--paper-input-container-underline-focus: {
|
||||
display: none;
|
||||
};
|
||||
--paper-input-container-underline-disabled: {
|
||||
display: none;
|
||||
};
|
||||
--paper-input-container-disabled: {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
paper-input:not([readonly]) {
|
||||
width: 60%;
|
||||
}
|
||||
</style>
|
||||
</template>
|
||||
</dom-module>
|
||||
|
|
@ -19,11 +19,14 @@
|
|||
<link rel='import' href='../bower_components/paper-checkbox/paper-checkbox.html'>
|
||||
|
||||
<link rel='import' href='./cloud-install-styles.html'>
|
||||
<link rel='import' href='./outline-server-settings-styles.html'>
|
||||
<link rel='import' href='./outline-iconset.html'>
|
||||
<link rel='import' href='./outline-validated-input.html'>
|
||||
|
||||
<dom-module id='outline-server-settings'>
|
||||
<template>
|
||||
<style include='cloud-install-styles'></style>
|
||||
<style include='outline-server-settings-styles'></style>
|
||||
<style>
|
||||
.content {
|
||||
flex-grow: 1;
|
||||
|
|
@ -60,11 +63,6 @@
|
|||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
}
|
||||
#portForNewAccessKeysContainer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.detail {
|
||||
margin-top: 0px;
|
||||
font-size: 12px;
|
||||
|
|
@ -72,27 +70,6 @@
|
|||
.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
#keysPortSaveButton {
|
||||
background: var(--primary-green);
|
||||
color: #fff;
|
||||
height: 36px;
|
||||
width: 83px;
|
||||
}
|
||||
#keysPortSaveButton[disabled] {
|
||||
background-color: rgba(255, 255, 255, .12);
|
||||
color: rgba(255, 255, 255, .3);
|
||||
}
|
||||
#keysPortCancelButton {
|
||||
color: #fff;
|
||||
width: 83px;
|
||||
}
|
||||
#keysPortCancelButton[disabled] {
|
||||
color:rgba(255, 255, 255, 0.3);
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
#portForNewAccessKeysContainer {
|
||||
max-width: 545px;
|
||||
}
|
||||
paper-checkbox {
|
||||
/* We want the ink to be the color we're going to, not coming from */
|
||||
--paper-checkbox-checked-color: var(--primary-green);
|
||||
|
|
@ -100,52 +77,10 @@
|
|||
--paper-checkbox-unchecked-color: var(--light-gray);
|
||||
--paper-checkbox-unchecked-ink-color: var(--primary-green);
|
||||
}
|
||||
/* Skip processing these with postcss-rtl as it incorrectly parses the border-color
|
||||
in the paper-input-container-underline-focus mixin. https://github.com/vkalinichev/postcss-rtl/issues/50 */
|
||||
/* rtl:begin:ignore */
|
||||
paper-input {
|
||||
/* Removes extra padding added by children of paper-input */
|
||||
margin-top: -8px;
|
||||
/* Create space for error messages */
|
||||
margin-bottom: 14px;
|
||||
--paper-input-container-label-focus: {
|
||||
color: var(--primary-green);
|
||||
};
|
||||
--paper-input-container-underline-focus: {
|
||||
border-color: var(--primary-green);
|
||||
};
|
||||
--paper-input-container-label: {
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
};
|
||||
--paper-input-container-color: var(--medium-gray);
|
||||
--paper-input-container-input: {
|
||||
color: #fff;
|
||||
};
|
||||
--paper-input-container-invalid-color: #f28b82;
|
||||
}
|
||||
/* rtl:end:ignore */
|
||||
paper-input[readonly] {
|
||||
--paper-input-container-underline: {
|
||||
display: none;
|
||||
};
|
||||
--paper-input-container-underline-focus: {
|
||||
display: none;
|
||||
};
|
||||
--paper-input-container-underline-disabled: {
|
||||
display: none;
|
||||
};
|
||||
--paper-input-container-disabled: {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
.link {
|
||||
text-decoration: underline;
|
||||
color: var(--medium-gray);
|
||||
}
|
||||
paper-input:not([readonly]) {
|
||||
width: 60%;
|
||||
}
|
||||
</style>
|
||||
<div class="container">
|
||||
<div class="content">
|
||||
|
|
@ -165,12 +100,12 @@
|
|||
<h3>[[localize('settings-server-info')]]</h3>
|
||||
<paper-input id="serverNameInput" class="server-name" value="{{name}}" label="[[localize('settings-server-name')]]" always-float-label maxlength="100" on-keydown="_handleNameInputKeyDown" on-blur="_handleNameInputBlur"></paper-input>
|
||||
<p class="detail">[[localize('settings-server-rename')]]</p>
|
||||
<div id="portForNewAccessKeysContainer">
|
||||
<paper-input id="keysPortInput" readonly$="[[!isAccessKeyPortEditable]]" allowed-pattern="[0-9]{1,5}" value="[[serverPortForNewAccessKeys]]" label="[[localize('settings-access-key-port')]]" maxlength="5" hidden$="[[!serverPortForNewAccessKeys]]" on-tap="setKeysPortEditing" on-keyup="_handleKeysPortInputKeyUp" always-float-label maxlength="100"></paper-input>
|
||||
<paper-button id="keysPortCancelButton" hidden$="[[!showKeysPortButtons]]" disabled$="[[!enableKeysPortButtons]]" on-tap="cancelKeysPortEdit">[[localize('cancel')]]</paper-button>
|
||||
<paper-button id="keysPortSaveButton" hidden$="[[!showKeysPortButtons]]" disabled$="[[!enableKeysPortButtons]]" on-tap="saveKeysPort">[[localize('save')]]</paper-button>
|
||||
</div>
|
||||
<paper-input readonly value="[[serverHostname]]" label="[[localize('settings-server-hostname')]]" hidden$="[[!serverHostname]]" always-float-label maxlength="100"></paper-input>
|
||||
<outline-validated-input
|
||||
editable="[[isAccessKeyPortEditable]]" visible="[[serverPortForNewAccessKeys]]" label="[[localize('settings-access-key-port')]]" allowed-pattern="[0-9]{1,5}" max-length="5" value="[[serverPortForNewAccessKeys]]" client-side-validator="[[_validatePort]]" event="ChangePortForNewAccessKeysRequested" localize="[[localize]]"
|
||||
></outline-validated-input>
|
||||
<outline-validated-input
|
||||
editable="[[isHostnameEditable]]" visible="[[serverHostname]]" label="[[localize('settings-server-hostname')]]" max-length="253" value="[[serverHostname]]" event="ChangeHostnameForAccessKeysRequested" localize="[[localize]]"
|
||||
></outline-validated-input>
|
||||
<paper-input readonly value="[[serverManagementApiUrl]]" label="[[localize('settings-server-api-url')]]" hidden$="[[!serverManagementApiUrl]]" always-float-label maxlength="100"></paper-input>
|
||||
<paper-input readonly value="[[serverCreationDate]]" label="[[localize('settings-server-creation')]]" hidden$="[[!serverCreationDate]]" always-float-label maxlength="100"></paper-input>
|
||||
<paper-input readonly value="[[serverId]]" label="[[localize('settings-server-id')]]" hidden$="[[!serverId]]" always-float-label maxlength="100"></paper-input>
|
||||
|
|
@ -203,6 +138,7 @@
|
|||
serverManagementApiUrl: {type: String, value: null},
|
||||
serverPortForNewAccessKeys: {type: Number, value: null},
|
||||
isAccessKeyPortEditable: {type: Boolean, value: false},
|
||||
isHostnameEditable: {type: Boolean, value: true},
|
||||
serverCreationDate: {type: String, value: null},
|
||||
serverLocation: {type: String, value: null},
|
||||
serverMonthlyCost: {type: String, value: null},
|
||||
|
|
@ -247,71 +183,11 @@
|
|||
this.fire(metricsSignal);
|
||||
}
|
||||
},
|
||||
setKeysPortEditing: function() {
|
||||
if (!this.isAccessKeyPortEditable) {
|
||||
return;
|
||||
}
|
||||
this.showKeysPortButtons = true;
|
||||
const invalid = this.$.keysPortInput.invalid;
|
||||
this.$.keysPortSaveButton.disabled = invalid;
|
||||
this.$.keysPortCancelButton.disabled = false;
|
||||
},
|
||||
setKeysPortSaved: function() {
|
||||
this.serverPortForNewAccessKeys = Number(this.$.keysPortInput.value);
|
||||
this.showKeysPortButtons = false;
|
||||
this.$.keysPortInput.readonly = false;
|
||||
},
|
||||
setKeysPortErrorState: function(message) {
|
||||
const input = this.$.keysPortInput;
|
||||
this.enableKeysPortButtons = true;
|
||||
input.invalid = true;
|
||||
input.errorMessage = message;
|
||||
input.readonly = false;
|
||||
input.focus();
|
||||
},
|
||||
saveKeysPort: function() {
|
||||
const input = this.$.keysPortInput;
|
||||
if (Number(input.value) === this.serverPortForNewAccessKeys) {
|
||||
this.cancelKeysPortEdit();
|
||||
return;
|
||||
};
|
||||
this.enableKeysPortButtons = false;
|
||||
input.readonly = true;
|
||||
input.invalid = false;
|
||||
this.fire('ChangePortForNewAccessKeysRequested', {
|
||||
port: Number(this.$.keysPortInput.value),
|
||||
serverSettings: this
|
||||
});
|
||||
},
|
||||
cancelKeysPortEdit: function() {
|
||||
this.$.keysPortInput.value = this.serverPortForNewAccessKeys;
|
||||
this.$.keysPortInput.invalid = false;
|
||||
this.showKeysPortButtons = false;
|
||||
},
|
||||
_handleKeysPortInputKeyUp: function(e) {
|
||||
let input = this.$.keysPortInput;
|
||||
const badInputMessage = this.localize('error-keys-port-bad-input');
|
||||
if (e.key === 'Escape') {
|
||||
this.cancelKeysPortEdit();
|
||||
input.blur();
|
||||
return;
|
||||
} else if (e.key === 'Enter') {
|
||||
if (!input.invalid) {
|
||||
this.saveKeysPort();
|
||||
input.blur();
|
||||
}
|
||||
return;
|
||||
}
|
||||
let port = Number(input.value);
|
||||
if (Number.isNaN(port) || (port < 1 || port > 65535) || !Number.isInteger(port)) {
|
||||
input.invalid = true;
|
||||
this.$.keysPortSaveButton.disabled = true;
|
||||
input.errorMessage = badInputMessage;
|
||||
} else {
|
||||
input.invalid = false;
|
||||
this.$.keysPortSaveButton.disabled = false;
|
||||
}
|
||||
}
|
||||
_validatePort: function(value) {
|
||||
const port = Number(value);
|
||||
const valid = !Number.isNaN(port) && port >= 1 && port <= 65535 && Number.isInteger(port);
|
||||
return valid ? "" : this.localize("error-keys-port-bad-input");
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</dom-module>
|
||||
|
|
|
|||
|
|
@ -499,6 +499,7 @@
|
|||
id="serverSettings"
|
||||
server-id="[[serverId]]"
|
||||
server-hostname="[[serverHostname]]"
|
||||
is-hostname-editable="[[isHostnameEditable]]"
|
||||
server-management-api-url="[[serverManagementApiUrl]]"
|
||||
server-port-for-new-access-keys="[[serverPortForNewAccessKeys]]"
|
||||
is-access-key-port-editable="[[isAccessKeyPortEditable]]"
|
||||
|
|
@ -539,6 +540,7 @@
|
|||
serverId: String,
|
||||
serverName: String,
|
||||
serverHostname: String,
|
||||
isHostnameEditable: {type: Boolean, value: false},
|
||||
serverManagementApiUrl: String,
|
||||
serverPortForNewAccessKeys: Number,
|
||||
isAccessKeyPortEditable: {type: Boolean, value: false},
|
||||
|
|
|
|||
191
src/server_manager/ui_components/outline-validated-input.html
Normal file
191
src/server_manager/ui_components/outline-validated-input.html
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
<!--
|
||||
Copyright 2020 The Outline Authors
|
||||
|
||||
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.
|
||||
-->
|
||||
|
||||
<!-- outline-validated-input
|
||||
This is an input, with a cancel and a save button, which performs client-side validation and has an
|
||||
event-based hook for server-side evaluation.
|
||||
|
||||
Attributes:
|
||||
* editable: Is this input editable? Default: false
|
||||
* visible: Is this input visible? Default: false
|
||||
* label: The label for the input. Default: null
|
||||
* value: The initial value entered in the input. SHOULD be a valid value. Default: null
|
||||
* allowed-pattern: Regex describing what inputs are allowed. Users will be prevented from
|
||||
entering inputs which don't follow the pattern. Default: ".*"
|
||||
* max-length: The number of characters allowed in the input. Default: Number.POSITIVE_INFINITY
|
||||
* client-side-validator: A function which takes a string and returns either an empty string on success
|
||||
or an error message on failure. This function will be called on every keystroke. Default: () => ""
|
||||
* event: The name of the event fired when the save button is tapped. Passes the
|
||||
current input value as "value" and the Polymer object as "ui". The handler for this event
|
||||
MUST call ui.setSavedState() or ui.setErrorState(message) depending on its result. Default: null
|
||||
-->
|
||||
|
||||
<link rel='import' href='./cloud-install-styles.html'>
|
||||
<link rel='import' href='./outline-server-settings-styles.html'>
|
||||
|
||||
<dom-module id='outline-validated-input'>
|
||||
<template>
|
||||
<style include='cloud-install-styles'></style>
|
||||
<style include='outline-server-settings-styles'></style>
|
||||
<style>
|
||||
#container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
max-width: 545px;
|
||||
}
|
||||
|
||||
paper-button {
|
||||
height: 36px;
|
||||
width: 83px;
|
||||
text-align:center
|
||||
}
|
||||
|
||||
paper-button[disabled] {
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
#saveButton {
|
||||
background: var(--primary-green);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
#cancelButton {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
#saveButton[disabled] {
|
||||
background-color: rgba(255, 255, 255, .12);
|
||||
}
|
||||
|
||||
#cancelButton[disabled] {
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
</style>
|
||||
<div id="container" hidden$="[[!visible]]">
|
||||
<!-- We use on-focus instead of on-tap to handle the case where text is selected but the tap event isn't fired. -->
|
||||
<paper-input id="input" readonly$="[[!editable]]" allowed-pattern="[[allowedPattern]]" value="[[value]]"
|
||||
label="[[label]]" maxlength="[[maxLength]]" on-focus="_enterEditingState" on-keyup="_onKeyUp"
|
||||
always-float-label>
|
||||
</paper-input>
|
||||
<paper-button id="cancelButton" hidden$="[[!_showButtons]]" disabled$="[[!_enableButtons]]"
|
||||
on-tap="_cancel">
|
||||
[[localize('cancel')]]
|
||||
</paper-button>
|
||||
<paper-button id="saveButton" hidden$="[[!_showButtons]]" disabled$="[[!_enableButtons]]" on-tap="_save">
|
||||
[[localize('save')]]
|
||||
</paper-button>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
Polymer({
|
||||
is: 'outline-validated-input',
|
||||
properties: {
|
||||
// Properties affecting the whole element
|
||||
editable: { type: Boolean, value: false },
|
||||
visible: { type: Boolean, value: false },
|
||||
// Properties affecting the input
|
||||
label: { type: String, readonly: true, value: null },
|
||||
allowedPattern: { type: String, value: ".*" },
|
||||
maxLength: { type: Number, value: Number.POSITIVE_INFINITY },
|
||||
value: { type: String, value: null },
|
||||
// `value` here is evaluated. If it were simply `() => "`, then clientSideValidator
|
||||
// would default to just "", not a function returning "".
|
||||
// Note also that we can't use paper-input's validator attribute because it will fight
|
||||
// with any server-side validation and cause unpredictable results.
|
||||
clientSideValidator: { type: Function, readonly: true, value: () => { return () => ""; } },
|
||||
// Properties affecting the buttons
|
||||
_showButtons: { type: Boolean, value: false },
|
||||
_enableButtons: { type: Boolean, value: false },
|
||||
// Other properties
|
||||
event: { type: String, value: null },
|
||||
localize: { type: Function, readonly: true }
|
||||
},
|
||||
_onKeyUp: function (e) {
|
||||
const input = this.$.input;
|
||||
if (e.key === 'Escape') {
|
||||
this._cancel();
|
||||
input.blur();
|
||||
return;
|
||||
} else if (e.key === 'Enter') {
|
||||
if (!input.invalid) {
|
||||
this._save();
|
||||
input.blur();
|
||||
}
|
||||
return;
|
||||
}
|
||||
const validationError = this.clientSideValidator(input.value);
|
||||
if (validationError) {
|
||||
input.invalid = true;
|
||||
this.$.saveButton.disabled = true;
|
||||
input.errorMessage = validationError;
|
||||
} else {
|
||||
input.invalid = false;
|
||||
this.$.saveButton.disabled = false;
|
||||
}
|
||||
},
|
||||
_cancel: function () {
|
||||
const input = this.$.input;
|
||||
input.value = this.value;
|
||||
input.invalid = false;
|
||||
this._showButtons = false;
|
||||
},
|
||||
_save: function () {
|
||||
const input = this.$.input;
|
||||
const value = input.value;
|
||||
if (value === this.value) {
|
||||
this._cancel();
|
||||
return;
|
||||
};
|
||||
this.$.cancelButton.disabled = true;
|
||||
this.$.saveButton.disabled = true;
|
||||
input.readonly = true;
|
||||
input.invalid = false;
|
||||
|
||||
// We use this heuristic to avoid having to pass a constructor as an attribute
|
||||
const numberValue = Number(value);
|
||||
const typedValue = Number.isNaN(numberValue) ? value : numberValue;
|
||||
|
||||
this.fire(this.event, {
|
||||
validatedInput: typedValue,
|
||||
ui: this
|
||||
});
|
||||
},
|
||||
_enterEditingState: function () {
|
||||
if (!this.editable) {
|
||||
return;
|
||||
}
|
||||
this._showButtons = true;
|
||||
this.$.cancelButton.disabled = false;
|
||||
this.$.saveButton.disabled = this.$.input.invalid;
|
||||
},
|
||||
enterSavedState: function () {
|
||||
const input = this.$.input;
|
||||
this.value = input.value;
|
||||
this._showButtons = false;
|
||||
input.readonly = false;
|
||||
},
|
||||
enterErrorState: function (message) {
|
||||
const input = this.$.input;
|
||||
this._enableButtons = true;
|
||||
input.errorMessage = message;
|
||||
input.invalid = true;
|
||||
input.readonly = false;
|
||||
input.focus();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</dom-module>
|
||||
|
|
@ -369,7 +369,10 @@ class FakeServer implements server.Server {
|
|||
removeAccessKey(accessKeyId: server.AccessKeyId) {
|
||||
return Promise.reject(new Error('FakeServer.removeAccessKey not implemented'));
|
||||
}
|
||||
getHostname() {
|
||||
setHostnameForAccessKeys(hostname: string) {
|
||||
return Promise.reject(new Error("FakeServer.setHostname not implemented"));
|
||||
}
|
||||
getHostnameForAccessKeys() {
|
||||
return 'fake-server';
|
||||
}
|
||||
getManagementApiUrl() {
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ interface PolymerEvent extends Event {
|
|||
const UNUSED_DIGITALOCEAN_REFERRAL_CODE = '5ddb4219b716';
|
||||
|
||||
const CHANGE_KEYS_PORT_VERSION = "1.0.0";
|
||||
const CHANGE_HOSTNAME_VERSION = "1.2.0";
|
||||
|
||||
interface UiAccessKey {
|
||||
id: string;
|
||||
|
|
@ -110,7 +111,11 @@ export class App {
|
|||
});
|
||||
|
||||
appRoot.addEventListener('ChangePortForNewAccessKeysRequested', (event: PolymerEvent) => {
|
||||
this.setPortForNewAccessKeys(event.detail.port, event.detail.serverSettings);
|
||||
this.setPortForNewAccessKeys(event.detail.validatedInput, event.detail.ui);
|
||||
});
|
||||
|
||||
appRoot.addEventListener('ChangeHostnameForAccessKeysRequested', (event: PolymerEvent) => {
|
||||
this.setHostnameForAccessKeys(event.detail.validatedInput, event.detail.ui);
|
||||
});
|
||||
|
||||
// The UI wants us to validate a server management URL.
|
||||
|
|
@ -761,11 +766,12 @@ export class App {
|
|||
view.isServerReachable = true;
|
||||
view.serverId = selectedServer.getServerId();
|
||||
view.serverName = selectedServer.getName();
|
||||
view.serverHostname = selectedServer.getHostname();
|
||||
view.serverHostname = selectedServer.getHostnameForAccessKeys();
|
||||
view.serverManagementApiUrl = selectedServer.getManagementApiUrl();
|
||||
view.serverPortForNewAccessKeys = selectedServer.getPortForNewAccessKeys();
|
||||
const version = this.selectedServer.getVersion();
|
||||
view.isAccessKeyPortEditable = version && semver.gte(version, CHANGE_KEYS_PORT_VERSION);
|
||||
view.isHostnameEditable = version && semver.gte(version, CHANGE_HOSTNAME_VERSION);
|
||||
view.serverCreationDate = selectedServer.getCreatedDate().toLocaleString(
|
||||
this.appRoot.language, {year: 'numeric', month: 'long', day: 'numeric'});
|
||||
|
||||
|
|
@ -920,24 +926,41 @@ export class App {
|
|||
});
|
||||
}
|
||||
|
||||
private async setHostnameForAccessKeys(hostname: string, serverSettings: Polymer) {
|
||||
this.appRoot.showNotification(this.appRoot.localize("saving"));
|
||||
try {
|
||||
await this.selectedServer.setHostnameForAccessKeys(hostname);
|
||||
this.appRoot.showNotification(this.appRoot.localize("saved"));
|
||||
serverSettings.enterSavedState();
|
||||
} catch (error) {
|
||||
this.appRoot.showError(this.appRoot.localize("error-not-saved"));
|
||||
if (error.isNetworkError()) {
|
||||
serverSettings.enterErrorState(this.appRoot.localize("error-network"));
|
||||
return;
|
||||
}
|
||||
const message = error.response.status === 400 ? "error-hostname-invalid" : "error-unexpected";
|
||||
serverSettings.enterErrorState(this.appRoot.localize(message));
|
||||
}
|
||||
}
|
||||
|
||||
private async setPortForNewAccessKeys(port: number, serverSettings: Polymer) {
|
||||
this.appRoot.showNotification(this.appRoot.localize("saving"));
|
||||
try {
|
||||
await this.selectedServer.setPortForNewAccessKeys(port);
|
||||
this.appRoot.showNotification(this.appRoot.localize("saved"));
|
||||
serverSettings.setKeysPortSaved();
|
||||
serverSettings.enterSavedState();
|
||||
} catch (error) {
|
||||
this.appRoot.showError(this.appRoot.localize("error-not-saved"));
|
||||
if (error.isNetworkError()) {
|
||||
serverSettings.setKeysPortErrorState(this.appRoot.localize("error-network"));
|
||||
serverSettings.enterErrorState(this.appRoot.localize("error-network"));
|
||||
return;
|
||||
}
|
||||
const code = error.response.status;
|
||||
if (code === 409) {
|
||||
serverSettings.setKeysPortErrorState(this.appRoot.localize("error-keys-port-in-use"));
|
||||
serverSettings.enterErrorState(this.appRoot.localize("error-keys-port-in-use"));
|
||||
return;
|
||||
}
|
||||
serverSettings.setKeysPortErrorState(this.appRoot.localize("error-unexpected"));
|
||||
serverSettings.enterErrorState(this.appRoot.localize("error-unexpected"));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ export interface DisplayServer {
|
|||
export async function makeDisplayServer(server: server.Server) {
|
||||
return {
|
||||
id: server.getManagementApiUrl(),
|
||||
name: await server.isHealthy().catch((e) => false) ? server.getName() : server.getHostname(),
|
||||
name: await server.isHealthy().catch((e) => false) ? server.getName() : server.getHostnameForAccessKeys(),
|
||||
isManaged: !!(server as server.ManagedServer).getHost,
|
||||
isSynced: true
|
||||
};
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ export interface ServerConfig {
|
|||
serverId: string;
|
||||
createdTimestampMs: number;
|
||||
portForNewAccessKeys: number;
|
||||
hostnameForAccessKeys: string;
|
||||
version: string;
|
||||
}
|
||||
|
||||
|
|
@ -129,12 +130,26 @@ export class ShadowboxServer implements server.Server {
|
|||
return new Date(this.serverConfig.createdTimestampMs);
|
||||
}
|
||||
|
||||
getHostname(): string {
|
||||
async setHostnameForAccessKeys(hostname: string): Promise<void> {
|
||||
console.info(`setHostname ${hostname}`);
|
||||
this.serverConfig.hostnameForAccessKeys = hostname;
|
||||
const requestOptions: RequestInit = {
|
||||
method: 'PUT',
|
||||
headers: new Headers({'Content-Type': 'application/json'}),
|
||||
body: JSON.stringify({hostname})
|
||||
};
|
||||
return this.apiRequest<void>('server/hostname-for-access-keys', requestOptions).then(() => {
|
||||
this.serverConfig.hostnameForAccessKeys = hostname;
|
||||
});
|
||||
}
|
||||
|
||||
getHostnameForAccessKeys(): string {
|
||||
try {
|
||||
return new URL(this.managementApiAddress).hostname;
|
||||
return this.serverConfig.hostnameForAccessKeys || new URL(this.managementApiAddress).hostname;
|
||||
} catch (e) {
|
||||
return '';
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
getPortForNewAccessKeys(): number|undefined {
|
||||
|
|
@ -149,7 +164,7 @@ export class ShadowboxServer implements server.Server {
|
|||
}
|
||||
|
||||
setPortForNewAccessKeys(newPort: number): Promise<void> {
|
||||
console.info(`setPortForNewAcessKeys: ${newPort}`);
|
||||
console.info(`setPortForNewAccessKeys: ${newPort}`);
|
||||
const requestOptions: RequestInit = {
|
||||
method: 'PUT',
|
||||
headers: new Headers({'Content-Type': 'application/json'}),
|
||||
|
|
|
|||
|
|
@ -160,7 +160,7 @@ client <-> shadowbox <-> target
|
|||
To test clients that rely on fetching a docker image from Dockerhub, you can push an image to your account and modify the
|
||||
client to use your image. To push your own image:
|
||||
```
|
||||
yarn shadowbox_docker_build && docker tag quay.io/outline/shadowbox $USER/shadowbox && docker push $USER/shadowbox
|
||||
yarn do shadowbox/docker/build && docker tag quay.io/outline/shadowbox $USER/shadowbox && docker push $USER/shadowbox
|
||||
```
|
||||
|
||||
If you need to test an unsigned image (e.g. your dev one):
|
||||
|
|
|
|||
|
|
@ -17,4 +17,4 @@
|
|||
do_action shadowbox/server/build
|
||||
|
||||
export DOCKER_CONTENT_TRUST=${DOCKER_CONTENT_TRUST:-1}
|
||||
docker build --force-rm --build-arg GITHUB_RELEASE="${TRAVIS_TAG:-none}" -t outline/shadowbox $ROOT_DIR -f src/shadowbox/docker/Dockerfile
|
||||
docker build --force-rm --build-arg GITHUB_RELEASE="${TRAVIS_TAG:-none}" -t ${SB_IMAGE:-outline/shadowbox} $ROOT_DIR -f src/shadowbox/docker/Dockerfile
|
||||
|
|
|
|||
|
|
@ -38,4 +38,4 @@ declare -a docker_bindings=(
|
|||
|
||||
echo "Running image ${SB_IMAGE}"
|
||||
|
||||
docker run --rm -it --network=host --name shadowbox "${docker_bindings[@]}" ${SB_IMAGE}
|
||||
docker run --rm -it --network=host --name shadowbox "${docker_bindings[@]}" ${SB_IMAGE:-outline/shadowbox}
|
||||
|
|
|
|||
|
|
@ -161,6 +161,18 @@ function cleanup() {
|
|||
if [[ "${ACCESS_KEY_JSON}" != *'"port":12345'* ]]; then
|
||||
fail "Port for new access keys wasn't changed. Newly created access key: ${ACCESS_KEY_JSON}"
|
||||
fi
|
||||
|
||||
# Verify that we can change the hostname for new access keys
|
||||
NEW_HOSTNAME="newhostname"
|
||||
client_curl --insecure -X PUT -H 'Content-Type: application/json' -d '{"hostname": "'${NEW_HOSTNAME}'"}' ${SB_API_URL}/server/hostname-for-access-keys \
|
||||
|| fail "Couldn't change hostname for new access keys"
|
||||
|
||||
ACCESS_KEY_JSON=$(client_curl --insecure -X POST ${SB_API_URL}/access-keys \
|
||||
|| fail "Couldn't get a new access key after changing hostname")
|
||||
|
||||
if [[ "${ACCESS_KEY_JSON}" != *"@${NEW_HOSTNAME}:"* ]]; then
|
||||
fail "Hostname for new access keys wasn't changed. Newly created access key: ${ACCESS_KEY_JSON}"
|
||||
fi
|
||||
|
||||
# Verify no errors occurred.
|
||||
readonly SHADOWBOX_LOG=$OUTPUT_DIR/shadowbox-log.txt
|
||||
|
|
|
|||
|
|
@ -55,6 +55,8 @@ export interface AccessKeyRepository {
|
|||
listAccessKeys(): AccessKey[];
|
||||
// Changes the port for new access keys.
|
||||
setPortForNewAccessKeys(port: number): Promise<void>;
|
||||
// Changes the hostname for access keys.
|
||||
setHostname(hostname: string): void;
|
||||
// Apply the specified update to the specified access key. Throws on failure.
|
||||
renameAccessKey(id: AccessKeyId, name: string): void;
|
||||
// Gets the metrics id for a given Access Key.
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "outline-server",
|
||||
"private": true,
|
||||
"version": "1.1.0",
|
||||
"version": "1.2.0",
|
||||
"description": "Outline server",
|
||||
"main": "build/server/main.js",
|
||||
"author": "Outline",
|
||||
|
|
@ -11,6 +11,7 @@
|
|||
],
|
||||
"dependencies": {
|
||||
"ShadowsocksConfig": "Jigsaw-Code/outline-shadowsocksconfig#^v0.0.9",
|
||||
"ip-regex": "^4.1.0",
|
||||
"js-yaml": "^3.12.0",
|
||||
"prom-client": "^11.1.3",
|
||||
"randomstring": "^1.1.5",
|
||||
|
|
|
|||
|
|
@ -27,7 +27,34 @@ paths:
|
|||
examples:
|
||||
'0':
|
||||
value: >-
|
||||
{"name":"My Server","serverId":"000-000-000-000","metricsEnabled":true,"createdTimestampMs":1536613192052,"portForNewAccessKeys":1234}
|
||||
{"name":"My Server","serverId":"000-000-000-000","metricsEnabled":true,"createdTimestampMs":1536613192052,"portForNewAccessKeys":1234, "version": "1.0.0"}
|
||||
/server/hostname-for-access-keys:
|
||||
put:
|
||||
tags:
|
||||
- Server
|
||||
description: Changes the hostname for access keys. Must be a valid hostname or IP address. If it's a hostname, DNS must be set up independently of this API.
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
hostname:
|
||||
type: string
|
||||
examples:
|
||||
'hostname':
|
||||
value: '{"hostname": www.example.org}'
|
||||
'IP address':
|
||||
value: '{"hostname": "127.0.0.1"}'
|
||||
responses:
|
||||
'204':
|
||||
description: The hostname was successfully changed.
|
||||
'400':
|
||||
description: An invalid hostname or IP address was provided.
|
||||
'500':
|
||||
description: An internal error occurred. This could be thrown if there were network errors while validating the hostname
|
||||
|
||||
/server/port-for-new-access-keys:
|
||||
put:
|
||||
description: Changes the default port for newly created access keys. This can be a port already used for access keys.
|
||||
|
|
@ -47,7 +74,7 @@ paths:
|
|||
value: '{"port": 12345}'
|
||||
responses:
|
||||
'204':
|
||||
description: The default port was sucessfully changed.
|
||||
description: The default port was successfully changed.
|
||||
'400':
|
||||
description: The requested port wasn't an integer from 1 through 65535, or the request had no port parameter.
|
||||
'409':
|
||||
|
|
@ -181,7 +208,7 @@ paths:
|
|||
- Access Key
|
||||
responses:
|
||||
'200':
|
||||
description: The data tranferred by each access key
|
||||
description: The data transferred by each access key
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
|
||||
import * as net from 'net';
|
||||
|
||||
import {InMemoryConfig} from '../infrastructure/json_config';
|
||||
import {InMemoryConfig, JsonConfig} from '../infrastructure/json_config';
|
||||
import {AccessKey, AccessKeyRepository, DataLimit} from '../model/access_key';
|
||||
|
||||
import {ShadowsocksManagerService} from './manager_service';
|
||||
|
|
@ -22,6 +22,7 @@ import {FakePrometheusClient, FakeShadowsocksServer} from './mocks/mocks';
|
|||
import {AccessKeyConfigJson, ServerAccessKeyRepository} from './server_access_key';
|
||||
import {ServerConfigJson} from './server_config';
|
||||
import {SharedMetricsPublisher} from './shared_metrics';
|
||||
import { ManagerMetrics } from './manager_metrics';
|
||||
|
||||
interface ServerInfo {
|
||||
name: string;
|
||||
|
|
@ -49,7 +50,7 @@ describe('ShadowsocksManagerService', () => {
|
|||
it('Return default name if name is absent', (done) => {
|
||||
const repo = getAccessKeyRepository();
|
||||
const serverConfig = new InMemoryConfig({} as ServerConfigJson);
|
||||
const service = new ShadowsocksManagerService('default name', serverConfig, repo, null, null);
|
||||
const service = new ShadowsocksManagerServiceBuilder().serverConfig(serverConfig).accessKeys(repo).build();
|
||||
service.getServer(
|
||||
{params: {}}, {
|
||||
send: (httpCode, data: ServerInfo) => {
|
||||
|
|
@ -63,9 +64,8 @@ describe('ShadowsocksManagerService', () => {
|
|||
it('Returns persisted properties', (done) => {
|
||||
const repo = getAccessKeyRepository();
|
||||
const accessKeyDataLimit = {bytes: 999};
|
||||
const serverConfig =
|
||||
new InMemoryConfig({name: 'Server', accessKeyDataLimit} as ServerConfigJson);
|
||||
const service = new ShadowsocksManagerService('default name', serverConfig, repo, null, null);
|
||||
const serverConfig = new InMemoryConfig({name: 'Server', accessKeyDataLimit} as ServerConfigJson);
|
||||
const service = new ShadowsocksManagerServiceBuilder().serverConfig(serverConfig).accessKeys(repo).build();
|
||||
service.getServer(
|
||||
{params: {}}, {
|
||||
send: (httpCode, data: ServerInfo) => {
|
||||
|
|
@ -83,7 +83,7 @@ describe('ShadowsocksManagerService', () => {
|
|||
it('Rename changes the server name', (done) => {
|
||||
const repo = getAccessKeyRepository();
|
||||
const serverConfig = new InMemoryConfig({} as ServerConfigJson);
|
||||
const service = new ShadowsocksManagerService('default name', serverConfig, repo, null, null);
|
||||
const service = new ShadowsocksManagerServiceBuilder().serverConfig(serverConfig).accessKeys(repo).build();
|
||||
service.renameServer(
|
||||
{params: {name: 'new name'}}, {
|
||||
send: (httpCode, _) => {
|
||||
|
|
@ -96,10 +96,91 @@ describe('ShadowsocksManagerService', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('setHostnameForAccessKeys', () => {
|
||||
it(`accepts valid hostnames`, (done) => {
|
||||
const serverConfig = new InMemoryConfig({} as ServerConfigJson);
|
||||
const service = new ShadowsocksManagerServiceBuilder().serverConfig(serverConfig).accessKeys(getAccessKeyRepository()).build();
|
||||
|
||||
const res = {
|
||||
send: (httpCode) => {
|
||||
expect(httpCode).toEqual(204);
|
||||
}
|
||||
};
|
||||
|
||||
const goodHostnames = [
|
||||
"-bad", "localhost","example.com","www.example.org","www.exa-mple.tw","123abc.co.uk","93.184.216.34","::0","2606:2800:220:1:248:1893:25c8:1946"
|
||||
];
|
||||
for (const hostname of goodHostnames) {
|
||||
service.setHostnameForAccessKeys({params: {hostname}}, res, () => {});
|
||||
}
|
||||
|
||||
responseProcessed = true;
|
||||
done();
|
||||
});
|
||||
it(`rejects invalid hostnames`, (done) => {
|
||||
const serverConfig = new InMemoryConfig({} as ServerConfigJson);
|
||||
const service = new ShadowsocksManagerServiceBuilder().serverConfig(serverConfig).accessKeys(getAccessKeyRepository()).build();
|
||||
|
||||
const res = { send: (httpCode) => {} };
|
||||
const next = (error) => {
|
||||
expect(error.statusCode).toEqual(400);
|
||||
};
|
||||
|
||||
|
||||
const badHostnames = [
|
||||
null, "","-abc.com","abc-.com","abc.com/def","i_have_underscores.net","gggg:ggg:220:1:248:1893:25c8:1946"
|
||||
];
|
||||
for (const hostname of badHostnames) {
|
||||
service.setHostnameForAccessKeys({params: {hostname}}, res, next);
|
||||
}
|
||||
|
||||
responseProcessed = true;
|
||||
done();
|
||||
});
|
||||
it('Changes the server\'s hostname', (done) => {
|
||||
const serverConfig = new InMemoryConfig({} as ServerConfigJson);
|
||||
const service = new ShadowsocksManagerServiceBuilder().serverConfig(serverConfig).accessKeys(getAccessKeyRepository()).build();
|
||||
const hostname = "www.example.org";
|
||||
const res = {
|
||||
send: (httpCode) => {
|
||||
expect(httpCode).toEqual(204);
|
||||
expect(serverConfig.data().hostname).toEqual(hostname);
|
||||
responseProcessed = true;
|
||||
}
|
||||
};
|
||||
service.setHostnameForAccessKeys({params: {hostname}}, res, done);
|
||||
});
|
||||
it('Rejects missing hostname', (done) => {
|
||||
const serverConfig = new InMemoryConfig({} as ServerConfigJson);
|
||||
const service = new ShadowsocksManagerServiceBuilder().serverConfig(serverConfig).accessKeys(getAccessKeyRepository()).build();
|
||||
const res = { send: (httpCode) => {} };
|
||||
const next = (error) => {
|
||||
expect(error.statusCode).toEqual(400);
|
||||
responseProcessed = true;
|
||||
done();
|
||||
};
|
||||
const missingHostname = { params: {} } as {params: {hostname: string}};
|
||||
service.setHostnameForAccessKeys(missingHostname, res, next);
|
||||
});
|
||||
it('Rejects non-string hostname', (done) => {
|
||||
const serverConfig = new InMemoryConfig({} as ServerConfigJson);
|
||||
const service = new ShadowsocksManagerServiceBuilder().serverConfig(serverConfig).accessKeys(getAccessKeyRepository()).build();
|
||||
const res = { send: (httpCode) => {} };
|
||||
const next = (error) => {
|
||||
expect(error.statusCode).toEqual(400);
|
||||
responseProcessed = true;
|
||||
done();
|
||||
};
|
||||
// tslint:disable-next-line: no-any
|
||||
const badHostname = ({ params: {hostname: 123} } as any ) as {params: {hostname: string}};
|
||||
service.setHostnameForAccessKeys(badHostname, res, next);
|
||||
});
|
||||
});
|
||||
|
||||
describe('listAccessKeys', () => {
|
||||
it('lists access keys in order', async (done) => {
|
||||
const repo = getAccessKeyRepository();
|
||||
const service = new ShadowsocksManagerService('default name', null, repo, null, null);
|
||||
const service = new ShadowsocksManagerServiceBuilder().accessKeys(repo).build();
|
||||
// Create 2 access keys with names.
|
||||
const key1 = await createNewAccessKeyWithName(repo, 'keyName1');
|
||||
const key2 = await createNewAccessKeyWithName(repo, 'keyName2');
|
||||
|
|
@ -119,7 +200,7 @@ describe('ShadowsocksManagerService', () => {
|
|||
});
|
||||
it('lists access keys with expected properties', async (done) => {
|
||||
const repo = getAccessKeyRepository();
|
||||
const service = new ShadowsocksManagerService('', null, repo, null, null);
|
||||
const service = new ShadowsocksManagerServiceBuilder().accessKeys(repo).build();
|
||||
const accessKey = await repo.createNewAccessKey();
|
||||
await repo.createNewAccessKey();
|
||||
const accessKeyName = 'new name';
|
||||
|
|
@ -143,7 +224,7 @@ describe('ShadowsocksManagerService', () => {
|
|||
describe('createNewAccessKey', () => {
|
||||
it('creates keys', (done) => {
|
||||
const repo = getAccessKeyRepository();
|
||||
const service = new ShadowsocksManagerService('default name', null, repo, null, null);
|
||||
const service = new ShadowsocksManagerServiceBuilder().accessKeys(repo).build();
|
||||
|
||||
// Verify that response returns a key with the expected properties.
|
||||
const res = {
|
||||
|
|
@ -158,7 +239,7 @@ describe('ShadowsocksManagerService', () => {
|
|||
it('Create returns a 500 when the repository throws an exception', (done) => {
|
||||
const repo = getAccessKeyRepository();
|
||||
spyOn(repo, 'createNewAccessKey').and.throwError('cannot write to disk');
|
||||
const service = new ShadowsocksManagerService('default name', null, repo, null, null);
|
||||
const service = new ShadowsocksManagerServiceBuilder().accessKeys(repo).build();
|
||||
|
||||
const res = {send: (httpCode, data) => {}};
|
||||
service.createNewAccessKey({params: {}}, res, (error) => {
|
||||
|
|
@ -172,7 +253,7 @@ describe('ShadowsocksManagerService', () => {
|
|||
it('changes ports for new access keys', async (done) => {
|
||||
const repo = getAccessKeyRepository();
|
||||
const serverConfig = new InMemoryConfig({} as ServerConfigJson);
|
||||
const service = new ShadowsocksManagerService('name', serverConfig, repo, null, null);
|
||||
const service = new ShadowsocksManagerServiceBuilder().serverConfig(serverConfig).accessKeys(repo).build();
|
||||
|
||||
const oldKey = await repo.createNewAccessKey();
|
||||
const res = {
|
||||
|
|
@ -191,7 +272,7 @@ describe('ShadowsocksManagerService', () => {
|
|||
it('changes the server config', async (done) => {
|
||||
const repo = getAccessKeyRepository();
|
||||
const serverConfig = new InMemoryConfig({} as ServerConfigJson);
|
||||
const service = new ShadowsocksManagerService('name', serverConfig, repo, null, null);
|
||||
const service = new ShadowsocksManagerServiceBuilder().serverConfig(serverConfig).accessKeys(repo).build();
|
||||
|
||||
const res = {
|
||||
send: (httpCode) => {
|
||||
|
|
@ -206,7 +287,7 @@ describe('ShadowsocksManagerService', () => {
|
|||
it('rejects invalid port numbers', async (done) => {
|
||||
const repo = getAccessKeyRepository();
|
||||
const serverConfig = new InMemoryConfig({} as ServerConfigJson);
|
||||
const service = new ShadowsocksManagerService('name', serverConfig, repo, null, null);
|
||||
const service = new ShadowsocksManagerServiceBuilder().serverConfig(serverConfig).accessKeys(repo).build();
|
||||
|
||||
const res = {
|
||||
send: (httpCode) => {
|
||||
|
|
@ -232,7 +313,7 @@ describe('ShadowsocksManagerService', () => {
|
|||
it('rejects port numbers already in use', async (done) => {
|
||||
const repo = getAccessKeyRepository();
|
||||
const serverConfig = new InMemoryConfig({} as ServerConfigJson);
|
||||
const service = new ShadowsocksManagerService('name', serverConfig, repo, null, null);
|
||||
const service = new ShadowsocksManagerServiceBuilder().serverConfig(serverConfig).accessKeys(repo).build();
|
||||
|
||||
const res = {
|
||||
send: (httpCode) => {
|
||||
|
|
@ -257,7 +338,7 @@ describe('ShadowsocksManagerService', () => {
|
|||
it('accepts port numbers already in use by access keys', async (done) => {
|
||||
const repo = getAccessKeyRepository();
|
||||
const serverConfig = new InMemoryConfig({} as ServerConfigJson);
|
||||
const service = new ShadowsocksManagerService('name', serverConfig, repo, null, null);
|
||||
const service = new ShadowsocksManagerServiceBuilder().serverConfig(serverConfig).accessKeys(repo).build();
|
||||
|
||||
await service.createNewAccessKey({params: {}}, {send: () => {}}, () => {});
|
||||
await service.setPortForNewAccessKeys({params: {port: NEW_PORT}}, {send: () => {}}, () => {});
|
||||
|
|
@ -279,7 +360,7 @@ describe('ShadowsocksManagerService', () => {
|
|||
it('rejects malformed requests', async (done) => {
|
||||
const repo = getAccessKeyRepository();
|
||||
const serverConfig = new InMemoryConfig({} as ServerConfigJson);
|
||||
const service = new ShadowsocksManagerService('name', serverConfig, repo, null, null);
|
||||
const service = new ShadowsocksManagerServiceBuilder().serverConfig(serverConfig).accessKeys(repo).build();
|
||||
|
||||
const noPort = {params: {}};
|
||||
const res = {
|
||||
|
|
@ -308,7 +389,7 @@ describe('ShadowsocksManagerService', () => {
|
|||
describe('removeAccessKey', () => {
|
||||
it('removes keys', async (done) => {
|
||||
const repo = getAccessKeyRepository();
|
||||
const service = new ShadowsocksManagerService('default name', null, repo, null, null);
|
||||
const service = new ShadowsocksManagerServiceBuilder().accessKeys(repo).build();
|
||||
const key1 = await repo.createNewAccessKey();
|
||||
const key2 = await repo.createNewAccessKey();
|
||||
const res = {
|
||||
|
|
@ -327,7 +408,7 @@ describe('ShadowsocksManagerService', () => {
|
|||
it('Remove returns a 500 when the repository throws an exception', async (done) => {
|
||||
const repo = getAccessKeyRepository();
|
||||
spyOn(repo, 'removeAccessKey').and.throwError('cannot write to disk');
|
||||
const service = new ShadowsocksManagerService('default name', null, repo, null, null);
|
||||
const service = new ShadowsocksManagerServiceBuilder().accessKeys(repo).build();
|
||||
const key = await createNewAccessKeyWithName(repo, 'keyName1');
|
||||
const res = {send: (httpCode, data) => {}};
|
||||
service.removeAccessKey({params: {id: key.id}}, res, (error) => {
|
||||
|
|
@ -341,7 +422,7 @@ describe('ShadowsocksManagerService', () => {
|
|||
describe('renameAccessKey', () => {
|
||||
it('renames keys', async (done) => {
|
||||
const repo = getAccessKeyRepository();
|
||||
const service = new ShadowsocksManagerService('default name', null, repo, null, null);
|
||||
const service = new ShadowsocksManagerServiceBuilder().accessKeys(repo).build();
|
||||
const OLD_NAME = 'oldName';
|
||||
const NEW_NAME = 'newName';
|
||||
|
||||
|
|
@ -358,7 +439,7 @@ describe('ShadowsocksManagerService', () => {
|
|||
});
|
||||
it('Rename returns a 400 when the access key id is not a string', async (done) => {
|
||||
const repo = getAccessKeyRepository();
|
||||
const service = new ShadowsocksManagerService('default name', null, repo, null, null);
|
||||
const service = new ShadowsocksManagerServiceBuilder().accessKeys(repo).build();
|
||||
|
||||
const key = await repo.createNewAccessKey();
|
||||
const res = {send: (httpCode, data) => {}};
|
||||
|
|
@ -371,7 +452,7 @@ describe('ShadowsocksManagerService', () => {
|
|||
it('Rename returns a 500 when the repository throws an exception', async (done) => {
|
||||
const repo = getAccessKeyRepository();
|
||||
spyOn(repo, 'renameAccessKey').and.throwError('cannot write to disk');
|
||||
const service = new ShadowsocksManagerService('default name', null, repo, null, null);
|
||||
const service = new ShadowsocksManagerServiceBuilder().accessKeys(repo).build();
|
||||
|
||||
const key = await createNewAccessKeyWithName(repo, 'oldName');
|
||||
const res = {send: (httpCode, data) => {}};
|
||||
|
|
@ -388,7 +469,7 @@ describe('ShadowsocksManagerService', () => {
|
|||
const serverConfig = new InMemoryConfig({} as ServerConfigJson);
|
||||
const repo = getAccessKeyRepository();
|
||||
spyOn(repo, 'setAccessKeyDataLimit');
|
||||
const service = new ShadowsocksManagerService('default name', serverConfig, repo, null, null);
|
||||
const service = new ShadowsocksManagerServiceBuilder().serverConfig(serverConfig).accessKeys(repo).build();
|
||||
const limit = {bytes: 10000};
|
||||
const res = {
|
||||
send: (httpCode, data) => {
|
||||
|
|
@ -410,7 +491,7 @@ describe('ShadowsocksManagerService', () => {
|
|||
});
|
||||
it('returns 400 when limit is missing values', async (done) => {
|
||||
const repo = getAccessKeyRepository();
|
||||
const service = new ShadowsocksManagerService('default name', null, repo, null, null);
|
||||
const service = new ShadowsocksManagerServiceBuilder().accessKeys(repo).build();
|
||||
const accessKey = await repo.createNewAccessKey();
|
||||
const limit = {} as DataLimit;
|
||||
const res = {send: (httpCode, data) => {}};
|
||||
|
|
@ -422,7 +503,7 @@ describe('ShadowsocksManagerService', () => {
|
|||
});
|
||||
it('returns 400 when limit has negative values', async (done) => {
|
||||
const repo = getAccessKeyRepository();
|
||||
const service = new ShadowsocksManagerService('default name', null, repo, null, null);
|
||||
const service = new ShadowsocksManagerServiceBuilder().accessKeys(repo).build();
|
||||
const accessKey = await repo.createNewAccessKey();
|
||||
const limit = {bytes: -1};
|
||||
const res = {send: (httpCode, data) => {}};
|
||||
|
|
@ -435,8 +516,8 @@ describe('ShadowsocksManagerService', () => {
|
|||
it('returns 500 when the repository throws an exception', async (done) => {
|
||||
const repo = getAccessKeyRepository();
|
||||
spyOn(repo, 'setAccessKeyDataLimit').and.throwError('cannot write to disk');
|
||||
const service = new ShadowsocksManagerService('default name', null, repo, null, null);
|
||||
const accessKey = await repo.createNewAccessKey();
|
||||
const service = new ShadowsocksManagerServiceBuilder().accessKeys(repo).build();
|
||||
await repo.createNewAccessKey();
|
||||
const limit = {bytes: 10000};
|
||||
const res = {send: (httpCode, data) => {}};
|
||||
service.setAccessKeyDataLimit({params: {limit}}, res, (error) => {
|
||||
|
|
@ -453,7 +534,7 @@ describe('ShadowsocksManagerService', () => {
|
|||
const serverConfig = new InMemoryConfig({'accessKeyDataLimit': limit} as ServerConfigJson);
|
||||
const repo = getAccessKeyRepository();
|
||||
spyOn(repo, 'removeAccessKeyDataLimit').and.callThrough();
|
||||
const service = new ShadowsocksManagerService('default name', serverConfig, repo, null, null);
|
||||
const service = new ShadowsocksManagerServiceBuilder().serverConfig(serverConfig).accessKeys(repo).build();
|
||||
await repo.setAccessKeyDataLimit(limit);
|
||||
const res = {
|
||||
send: (httpCode, data) => {
|
||||
|
|
@ -468,7 +549,7 @@ describe('ShadowsocksManagerService', () => {
|
|||
it('returns 500 when the repository throws an exception', async (done) => {
|
||||
const repo = getAccessKeyRepository();
|
||||
spyOn(repo, 'removeAccessKeyDataLimit').and.throwError('cannot write to disk');
|
||||
const service = new ShadowsocksManagerService('default name', null, repo, null, null);
|
||||
const service = new ShadowsocksManagerServiceBuilder().accessKeys(repo).build();
|
||||
const accessKey = await repo.createNewAccessKey();
|
||||
const res = {send: (httpCode, data) => {}};
|
||||
service.removeAccessKeyDataLimit({params: {id: accessKey.id}}, res, (error) => {
|
||||
|
|
@ -483,8 +564,7 @@ describe('ShadowsocksManagerService', () => {
|
|||
it('Returns value from sharedMetrics', (done) => {
|
||||
const sharedMetrics = fakeSharedMetricsReporter();
|
||||
sharedMetrics.startSharing();
|
||||
const service =
|
||||
new ShadowsocksManagerService('default name', null, null, null, sharedMetrics);
|
||||
const service = new ShadowsocksManagerServiceBuilder().metricsPublisher(sharedMetrics).build();
|
||||
service.getShareMetrics(
|
||||
{params: {}}, {
|
||||
send: (httpCode, data: {metricsEnabled: boolean}) => {
|
||||
|
|
@ -500,8 +580,7 @@ describe('ShadowsocksManagerService', () => {
|
|||
it('Sets value in the config', (done) => {
|
||||
const sharedMetrics = fakeSharedMetricsReporter();
|
||||
sharedMetrics.stopSharing();
|
||||
const service =
|
||||
new ShadowsocksManagerService('default name', null, null, null, sharedMetrics);
|
||||
const service = new ShadowsocksManagerServiceBuilder().metricsPublisher(sharedMetrics).build();
|
||||
service.setShareMetrics(
|
||||
{params: {metricsEnabled: true}}, {
|
||||
send: (httpCode, _) => {
|
||||
|
|
@ -515,6 +594,43 @@ describe('ShadowsocksManagerService', () => {
|
|||
});
|
||||
});
|
||||
|
||||
class ShadowsocksManagerServiceBuilder {
|
||||
private defaultServerName_ = 'default name';
|
||||
private serverConfig_: JsonConfig<ServerConfigJson> = null;
|
||||
private accessKeys_: AccessKeyRepository = null;
|
||||
private managerMetrics_: ManagerMetrics = null;
|
||||
private metricsPublisher_: SharedMetricsPublisher = null;
|
||||
|
||||
defaultServerName(name: string): ShadowsocksManagerServiceBuilder {
|
||||
this.defaultServerName_ = name;
|
||||
return this;
|
||||
}
|
||||
|
||||
serverConfig(config: JsonConfig<ServerConfigJson>): ShadowsocksManagerServiceBuilder {
|
||||
this.serverConfig_ = config;
|
||||
return this;
|
||||
}
|
||||
|
||||
accessKeys(keys: AccessKeyRepository): ShadowsocksManagerServiceBuilder {
|
||||
this.accessKeys_ = keys;
|
||||
return this;
|
||||
}
|
||||
|
||||
managerMetrics(metrics: ManagerMetrics): ShadowsocksManagerServiceBuilder {
|
||||
this.managerMetrics_ = metrics;
|
||||
return this;
|
||||
}
|
||||
|
||||
metricsPublisher(publisher: SharedMetricsPublisher): ShadowsocksManagerServiceBuilder {
|
||||
this.metricsPublisher_ = publisher;
|
||||
return this;
|
||||
}
|
||||
|
||||
build(): ShadowsocksManagerService {
|
||||
return new ShadowsocksManagerService(this.defaultServerName_, this.serverConfig_, this.accessKeys_, this.managerMetrics_, this.metricsPublisher_);
|
||||
}
|
||||
}
|
||||
|
||||
async function createNewAccessKeyWithName(
|
||||
repo: AccessKeyRepository, name: string): Promise<AccessKey> {
|
||||
const accessKey = await repo.createNewAccessKey();
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
// limitations under the License.
|
||||
|
||||
import * as restify from 'restify';
|
||||
import * as ipRegex from 'ip-regex';
|
||||
import {makeConfig, SIP002_URI} from 'ShadowsocksConfig/shadowsocks_config';
|
||||
|
||||
import {JsonConfig} from '../infrastructure/json_config';
|
||||
|
|
@ -75,6 +76,7 @@ export function bindService(
|
|||
apiServer: restify.Server, apiPrefix: string, service: ShadowsocksManagerService) {
|
||||
apiServer.put(`${apiPrefix}/name`, service.renameServer.bind(service));
|
||||
apiServer.get(`${apiPrefix}/server`, service.getServer.bind(service));
|
||||
apiServer.put(`${apiPrefix}/server/hostname-for-access-keys`, service.setHostnameForAccessKeys.bind(service));
|
||||
apiServer.put(
|
||||
`${apiPrefix}/server/port-for-new-access-keys`,
|
||||
service.setPortForNewAccessKeys.bind(service));
|
||||
|
|
@ -142,12 +144,44 @@ export class ShadowsocksManagerService {
|
|||
metricsEnabled: this.serverConfig.data().metricsEnabled || false,
|
||||
createdTimestampMs: this.serverConfig.data().createdTimestampMs,
|
||||
version,
|
||||
accessKeyDataLimit: this.serverConfig.data().accessKeyDataLimit,
|
||||
portForNewAccessKeys: this.serverConfig.data().portForNewAccessKeys,
|
||||
accessKeyDataLimit: this.serverConfig.data().accessKeyDataLimit
|
||||
hostnameForAccessKeys: this.serverConfig.data().hostname
|
||||
});
|
||||
next();
|
||||
}
|
||||
|
||||
// Changes the server's hostname. Hostname must be a valid domain or IP address
|
||||
public setHostnameForAccessKeys(req: RequestType, res: ResponseType, next: restify.Next): void {
|
||||
logging.debug(`changeHostname request: ${JSON.stringify(req.params)}`);
|
||||
|
||||
const hostname = req.params.hostname;
|
||||
if (typeof hostname === 'undefined') {
|
||||
return next(new restify.MissingParameterError({statusCode: 400}, "hostname must be provided"));
|
||||
}
|
||||
if (typeof hostname !== 'string') {
|
||||
return next(
|
||||
new restify.InvalidArgumentError(
|
||||
{statusCode: 400},
|
||||
`Expected hostname to be a string, instead got ${hostname} of type ${typeof hostname}`));
|
||||
}
|
||||
// Hostnames can have any number of segments of alphanumeric characters and hyphens, separated by periods.
|
||||
// No segment may start or end with a hyphen.
|
||||
const hostnameRegex = /^([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?\.)*[A-Za-z0-9]([A-Za-z0-9\-]*[A-Za-z0-9])?$/;
|
||||
if (!hostnameRegex.test(hostname) && !ipRegex({includeBoundaries: true}).test(hostname)) {
|
||||
return next(
|
||||
new restify.InvalidArgumentError(
|
||||
{statusCode: 400},
|
||||
`Hostname ${hostname} isn't a valid hostname or IP address`));
|
||||
}
|
||||
|
||||
this.serverConfig.data().hostname = hostname;
|
||||
this.serverConfig.write();
|
||||
this.accessKeys.setHostname(hostname);
|
||||
res.send(HttpSuccess.NO_CONTENT);
|
||||
next();
|
||||
}
|
||||
|
||||
// Lists all access keys
|
||||
public listAccessKeys(req: RequestType, res: ResponseType, next: restify.Next): void {
|
||||
logging.debug(`listAccessKeys request ${JSON.stringify(req.params)}`);
|
||||
|
|
|
|||
|
|
@ -385,6 +385,15 @@ describe('ServerAccessKeyRepository', () => {
|
|||
expect(serverAccessKeys[1].id).toEqual(accessKey2.id);
|
||||
done();
|
||||
});
|
||||
|
||||
it('setHostname changes hostname for new keys', async (done) => {
|
||||
const newHostname = "host2";
|
||||
const repo = new RepoBuilder().build();
|
||||
repo.setHostname(newHostname);
|
||||
const key = await repo.createNewAccessKey();
|
||||
expect(key.proxyParams.hostname).toEqual(newHostname);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
// Convenience function to expect that an asynchronous function does not throw an error. Note that
|
||||
|
|
|
|||
|
|
@ -125,6 +125,10 @@ export class ServerAccessKeyRepository implements AccessKeyRepository {
|
|||
});
|
||||
}
|
||||
|
||||
setHostname(hostname: string): void {
|
||||
this.proxyHostname = hostname;
|
||||
}
|
||||
|
||||
async setPortForNewAccessKeys(port: number): Promise<void> {
|
||||
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
||||
throw new errors.InvalidPortNumber(port.toString());
|
||||
|
|
|
|||
|
|
@ -3515,10 +3515,10 @@ invert-kv@^1.0.0:
|
|||
resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6"
|
||||
integrity sha1-EEqOSqym09jNFXqO+L+rLXo//bY=
|
||||
|
||||
ip-regex@^1.0.1:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-1.0.3.tgz#dc589076f659f419c222039a33316f1c7387effd"
|
||||
integrity sha1-3FiQdvZZ9BnCIgOaMzFvHHOH7/0=
|
||||
ip-regex@^4.1.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-4.1.0.tgz#5ad62f685a14edb421abebc2fff8db94df67b455"
|
||||
integrity sha512-pKnZpbgCTfH/1NLIlOduP/V+WRXzC2MOz3Qo8xmxk8C5GudJLgK5QyLVXOSWy3ParAH7Eemurl3xjv/WXYFvMA==
|
||||
|
||||
ipaddr.js@1.9.0:
|
||||
version "1.9.0"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue