From 46422c7c030c850c91815a9c78c55d66de27fda6 Mon Sep 17 00:00:00 2001 From: Daniel LaCosse <3759828+daniellacosse@users.noreply.github.com> Date: Thu, 1 Feb 2024 20:49:03 -0500 Subject: [PATCH 1/3] proposal: add a provider message and contact option in the dynamic key --- Makefile | 5 +- commitlint.config.js | 5 ++ src/electron/go_vpn_tunnel.ts | 2 +- src/electron/index.ts | 2 +- src/electron/tunnel_store.ts | 2 +- src/www/app/app.ts | 21 +++++++- src/www/app/cordova_main.ts | 2 +- src/www/app/electron_outline_tunnel.ts | 3 +- src/www/app/fake_tunnel.ts | 3 +- .../access_key_serialization.ts | 6 ++- .../app/outline_server_repository/server.ts | 5 +- src/www/app/tunnel.ts | 8 +-- src/www/model/server.ts | 5 ++ src/www/model/shadowsocks_session_config.ts | 22 ++++++++ src/www/style.css | 1 + .../views/servers_view/server_list/index.ts | 4 +- .../servers_view/server_list_item/index.ts | 7 +++ .../server_list_item/server_card/index.ts | 54 +++++++++++++++++-- 18 files changed, 131 insertions(+), 26 deletions(-) create mode 100644 src/www/model/shadowsocks_session_config.ts diff --git a/Makefile b/Makefile index 9a9bf3a245..0d49b9e717 100644 --- a/Makefile +++ b/Makefile @@ -7,7 +7,7 @@ GOBIND=env PATH="$(GOBIN):$(PATH)" "$(GOMOBILE)" bind IMPORT_HOST=github.com IMPORT_PATH=$(IMPORT_HOST)/Jigsaw-Code/outline-client -.PHONY: android apple linux windows +.PHONY: android apple linux windows browser all: android apple linux windows @@ -74,3 +74,6 @@ $(XGO): go.mod go.mod: tools.go go mod tidy touch go.mod + +browser: + echo 'browser environment: nothing to do' diff --git a/commitlint.config.js b/commitlint.config.js index d53855c5e3..a123704e62 100644 --- a/commitlint.config.js +++ b/commitlint.config.js @@ -22,5 +22,10 @@ module.exports = { 'service/windows', ], ], + 'type-enum': [ + 2, + 'always', + ['build', 'chore', 'ci', 'docs', 'feat', 'fix', 'perf', 'proposal', 'refactor', 'revert', 'style', 'test'], + ], }, }; diff --git a/src/electron/go_vpn_tunnel.ts b/src/electron/go_vpn_tunnel.ts index 33d11bb2f2..dff3714546 100755 --- a/src/electron/go_vpn_tunnel.ts +++ b/src/electron/go_vpn_tunnel.ts @@ -16,7 +16,7 @@ import {powerMonitor} from 'electron'; import {platform} from 'os'; import {pathToEmbeddedBinary} from '../infrastructure/electron/app_paths'; -import {ShadowsocksSessionConfig} from '../www/app/tunnel'; +import {ShadowsocksSessionConfig} from '../www/model/shadowsocks_session_config'; import {TunnelStatus} from '../www/app/tunnel'; import {ErrorCode, fromErrorCode, UnexpectedPluginError} from '../www/model/errors'; diff --git a/src/electron/index.ts b/src/electron/index.ts index 60820e502a..e69857daf4 100644 --- a/src/electron/index.ts +++ b/src/electron/index.ts @@ -25,7 +25,7 @@ import autoLaunch = require('auto-launch'); // tslint:disable-line import * as errors from '../www/model/errors'; -import {ShadowsocksSessionConfig} from '../www/app/tunnel'; +import {ShadowsocksSessionConfig} from '../www/model/shadowsocks_session_config'; import {TunnelStatus} from '../www/app/tunnel'; import {GoVpnTunnel} from './go_vpn_tunnel'; import {installRoutingServices, RoutingDaemon} from './routing_service'; diff --git a/src/electron/tunnel_store.ts b/src/electron/tunnel_store.ts index b920f765d1..e78b775087 100755 --- a/src/electron/tunnel_store.ts +++ b/src/electron/tunnel_store.ts @@ -15,7 +15,7 @@ import * as fs from 'fs'; import * as path from 'path'; -import {ShadowsocksSessionConfig} from '../www/app/tunnel'; +import {ShadowsocksSessionConfig} from '../www/model/shadowsocks_session_config'; // Format to store a tunnel configuration. export interface SerializableTunnel { diff --git a/src/www/app/app.ts b/src/www/app/app.ts index a22c3cd10d..65e712d965 100644 --- a/src/www/app/app.ts +++ b/src/www/app/app.ts @@ -589,7 +589,7 @@ export class App { // Helpers: private makeServerListItem(server: Server): ServerListItem { - return { + const serverListItem: ServerListItem = { disabled: false, errorMessageId: server.errorMessageId, isOutlineServer: server.isOutlineServer, @@ -598,6 +598,25 @@ export class App { id: server.id, connectionState: ServerConnectionState.DISCONNECTED, }; + + if (server.sessionConfig?.extra) { + const extraParams = server.sessionConfig.extra; + + if (['error', 'warning', 'info'].includes(extraParams.messageType) && extraParams.messageContent) { + serverListItem.message = { + type: extraParams.messageType as 'error' | 'warning' | 'info', + content: extraParams.messageContent, + }; + } + + if (extraParams.contactEmail) { + serverListItem.contact = { + email: extraParams.email, + }; + } + } + + return serverListItem; } private throttleServerConnectionChange(serverId: string, time: number) { diff --git a/src/www/app/cordova_main.ts b/src/www/app/cordova_main.ts index 75c3372a31..ee21ca5fbd 100644 --- a/src/www/app/cordova_main.ts +++ b/src/www/app/cordova_main.ts @@ -34,7 +34,7 @@ import {Tunnel, TunnelStatus} from './tunnel'; import {AbstractUpdater} from './updater'; import * as interceptors from './url_interceptor'; import {FakeOutlineTunnel} from './fake_tunnel'; -import {ShadowsocksSessionConfig} from './tunnel'; +import {ShadowsocksSessionConfig} from '../model/shadowsocks_session_config'; import {NoOpVpnInstaller, VpnInstaller} from './vpn_installer'; const OUTLINE_PLUGIN_NAME = 'OutlinePlugin'; diff --git a/src/www/app/electron_outline_tunnel.ts b/src/www/app/electron_outline_tunnel.ts index 55d7d8c076..e2c4d0b1fb 100644 --- a/src/www/app/electron_outline_tunnel.ts +++ b/src/www/app/electron_outline_tunnel.ts @@ -14,7 +14,8 @@ import * as errors from '../model/errors'; -import {Tunnel, TunnelStatus, ShadowsocksSessionConfig} from './tunnel'; +import {Tunnel, TunnelStatus} from './tunnel'; +import {ShadowsocksSessionConfig} from '../model/shadowsocks_session_config'; export class ElectronOutlineTunnel implements Tunnel { private statusChangeListener: ((status: TunnelStatus) => void) | null = null; diff --git a/src/www/app/fake_tunnel.ts b/src/www/app/fake_tunnel.ts index 54274f6068..3550267c40 100644 --- a/src/www/app/fake_tunnel.ts +++ b/src/www/app/fake_tunnel.ts @@ -14,7 +14,8 @@ import * as errors from '../model/errors'; -import {Tunnel, TunnelStatus, ShadowsocksSessionConfig} from './tunnel'; +import {Tunnel, TunnelStatus} from './tunnel'; +import {ShadowsocksSessionConfig} from '../model/shadowsocks_session_config'; // Fake Tunnel implementation for demoing and testing. // Note that because this implementation does not emit disconnection events, "switching" between diff --git a/src/www/app/outline_server_repository/access_key_serialization.ts b/src/www/app/outline_server_repository/access_key_serialization.ts index 17dcc560ce..0d3ba4fd80 100644 --- a/src/www/app/outline_server_repository/access_key_serialization.ts +++ b/src/www/app/outline_server_repository/access_key_serialization.ts @@ -16,7 +16,7 @@ import {SHADOWSOCKS_URI} from 'ShadowsocksConfig'; import * as errors from '../../model/errors'; -import {ShadowsocksSessionConfig} from '../tunnel'; +import {ShadowsocksSessionConfig} from '../../model/shadowsocks_session_config'; // DON'T use these methods outside of this folder! @@ -30,6 +30,7 @@ export function staticKeyToShadowsocksSessionConfig(staticKey: string): Shadowso method: config.method.data, password: config.password.data, prefix: config.extra?.['prefix'], + extra: config.extra, }; } catch (cause) { throw new errors.ServerAccessKeyInvalid('Invalid static access key.', {cause}); @@ -37,7 +38,7 @@ export function staticKeyToShadowsocksSessionConfig(staticKey: string): Shadowso } function parseShadowsocksSessionConfigJson(maybeJsonText: string): ShadowsocksSessionConfig | null { - const {method, password, server, server_port, prefix} = JSON.parse(maybeJsonText); + const {method, password, server, server_port, prefix, extra} = JSON.parse(maybeJsonText); // These are the mandatory keys. const missingKeys = []; @@ -58,6 +59,7 @@ function parseShadowsocksSessionConfigJson(maybeJsonText: string): ShadowsocksSe host: server, port: server_port, prefix, + extra, }; } diff --git a/src/www/app/outline_server_repository/server.ts b/src/www/app/outline_server_repository/server.ts index ed94aa24ee..b5d76f5ff6 100644 --- a/src/www/app/outline_server_repository/server.ts +++ b/src/www/app/outline_server_repository/server.ts @@ -15,8 +15,9 @@ import * as errors from '../../model/errors'; import * as events from '../../model/events'; import {Server, ServerType} from '../../model/server'; +import {ShadowsocksSessionConfig} from '../../model/shadowsocks_session_config'; -import {Tunnel, TunnelStatus, ShadowsocksSessionConfig} from '../tunnel'; +import {Tunnel, TunnelStatus} from '../tunnel'; import {fetchShadowsocksSessionConfig, staticKeyToShadowsocksSessionConfig} from './access_key_serialization'; @@ -28,7 +29,7 @@ export class OutlineServer implements Server { private static readonly SUPPORTED_CIPHERS = ['chacha20-ietf-poly1305', 'aes-128-gcm', 'aes-192-gcm', 'aes-256-gcm']; errorMessageId?: string; - private sessionConfig?: ShadowsocksSessionConfig; + sessionConfig?: ShadowsocksSessionConfig; constructor( public readonly id: string, diff --git a/src/www/app/tunnel.ts b/src/www/app/tunnel.ts index fc475e2a21..8564d69176 100644 --- a/src/www/app/tunnel.ts +++ b/src/www/app/tunnel.ts @@ -12,13 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -export interface ShadowsocksSessionConfig { - host?: string; - port?: number; - password?: string; - method?: string; - prefix?: string; -} +import {ShadowsocksSessionConfig} from '../model/shadowsocks_session_config'; export const enum TunnelStatus { CONNECTED, diff --git a/src/www/model/server.ts b/src/www/model/server.ts index f789f78331..4fee4bb8ae 100644 --- a/src/www/model/server.ts +++ b/src/www/model/server.ts @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +import {ShadowsocksSessionConfig} from './shadowsocks_session_config'; + // TODO: add guidelines for this file export enum ServerType { @@ -34,6 +36,9 @@ export interface Server { // The name of this server, as given by the user. name: string; + // The configuration used to connect to the server. + sessionConfig?: ShadowsocksSessionConfig; + // The location to pull the session config from on each connection. sessionConfigLocation?: URL; diff --git a/src/www/model/shadowsocks_session_config.ts b/src/www/model/shadowsocks_session_config.ts new file mode 100644 index 0000000000..542449c53b --- /dev/null +++ b/src/www/model/shadowsocks_session_config.ts @@ -0,0 +1,22 @@ +// Copyright 2024 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. + +export interface ShadowsocksSessionConfig { + host?: string; + port?: number; + password?: string; + method?: string; + prefix?: string; + extra?: {[key: string]: string}; +} diff --git a/src/www/style.css b/src/www/style.css index 01eb8c76c6..48e537fcb0 100644 --- a/src/www/style.css +++ b/src/www/style.css @@ -27,6 +27,7 @@ --min-supported-device-width: 320px; --outline-primary: hsl(170, 60%, 46%); + --outline-warning: hsl(48, 52%, 53%); --outline-error: hsl(4, 90%, 58%); --outline-black: hsl(0, 0%, 0%); diff --git a/src/www/views/servers_view/server_list/index.ts b/src/www/views/servers_view/server_list/index.ts index 0accdc8e66..763fcde7e2 100644 --- a/src/www/views/servers_view/server_list/index.ts +++ b/src/www/views/servers_view/server_list/index.ts @@ -34,11 +34,11 @@ export class ServerList extends PolymerElement { server-row-card { margin: 0 auto 8px auto; - height: 130px; + height: 200px; } server-hero-card { - height: 400px; + height: 500px; } diff --git a/src/www/views/servers_view/server_list_item/index.ts b/src/www/views/servers_view/server_list_item/index.ts index be1086e1ca..c518284ba3 100644 --- a/src/www/views/servers_view/server_list_item/index.ts +++ b/src/www/views/servers_view/server_list_item/index.ts @@ -34,6 +34,13 @@ export interface ServerListItem { id: string; name: string; connectionState: ServerConnectionState; + message?: { + type: 'error' | 'warning' | 'info'; + content: string; + }; + contact?: { + email: string; + }; } /** diff --git a/src/www/views/servers_view/server_list_item/server_card/index.ts b/src/www/views/servers_view/server_list_item/server_card/index.ts index 52a218a52f..3ca7887526 100644 --- a/src/www/views/servers_view/server_list_item/server_card/index.ts +++ b/src/www/views/servers_view/server_list_item/server_card/index.ts @@ -123,11 +123,41 @@ const sharedCSS = css` grid-area: footer; padding: var(--outline-mini-gutter) var(--outline-gutter); text-align: end; + display: flex; + justify-content: space-between; + align-items: center; } - .card-error { + .card-footer-button { + align-self: end; + } + + .card-error, + .card-provider-message { + font-family: var(--outline-font-family); + } + + .card-error, + .card-provider-message-error { color: var(--outline-error); - margin: 0 var(--outline-slim-gutter); + } + + .card-provider-message-warning { + color: var(--outline-warning); + } + + .card-provider-message-warning::before { + content: '⚠️ '; + } + + .card-provider-message-info, + .card-provider-message-contact { + color: var(--outline-primary); + } + + .card-provider-message-contact { + cursor: pointer; + text-decoration: underline; } `; @@ -140,13 +170,18 @@ const getSharedComponents = (element: ServerListItemElement & LitElement) => { ServerConnectionState.RECONNECTING, ].includes(server.connectionState); const hasErrorMessage = Boolean(server.errorMessageId); - - const messages = { + const messages: {[key: string]: string} = { serverName: server.name, error: hasErrorMessage ? localize(server.errorMessageId) : '', connectButton: localize(isConnectedState ? 'disconnect-button-label' : 'connect-button-label'), }; + if (Boolean(server.message && server.contact) && !hasErrorMessage) { + messages.providerMessageType = server.message.type; + messages.providerMessage = localize(server.message.content); + messages.providerEmail = server.contact.email; + } + const dispatchers = { beginRename: () => element.dispatchEvent( @@ -211,8 +246,17 @@ const getSharedComponents = (element: ServerListItemElement & LitElement) => { `, footer: html`