Skip to content

Commit

Permalink
Progress
Browse files Browse the repository at this point in the history
  • Loading branch information
fortuna committed Dec 10, 2024
1 parent bc20623 commit cde35c5
Show file tree
Hide file tree
Showing 6 changed files with 230 additions and 269 deletions.
14 changes: 14 additions & 0 deletions client/go/outline/config/module_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,17 @@ func TestRegisterParseURL(t *testing.T) {
require.Equal(t, "example.com:4321", d.FirstHop)
require.Equal(t, ConnTypeTunneled, d.ConnType)
}

func TestRegisterParseURLInQuotes(t *testing.T) {
providers := RegisterDefaultProviders(NewProviderContainer())

node, err := ParseConfigYAML(`"ss://[email protected]:4321/#My%20Server"`)
require.NoError(t, err)

d, err := providers.StreamDialers.NewInstance(context.Background(), node)
require.NoError(t, err)

require.NotNil(t, d.Dial)
require.Equal(t, "example.com:4321", d.FirstHop)
require.Equal(t, ConnTypeTunneled, d.ConnType)
}
63 changes: 47 additions & 16 deletions client/src/www/app/outline_server_repository/config.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@
// 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.
/*
import {makeConfig, SIP002_URI} from 'ShadowsocksConfig';

import * as config from './config';
import * as method_channel from '../method_channel';

/*
describe('getAddressFromTransport', () => {
it('extracts address', () => {
expect(
Expand Down Expand Up @@ -146,24 +146,55 @@ describe('parseTunnelConfig', () => {
});
});
});
*/

describe('serviceNameFromAccessKey', () => {
it('extracts name from ss:// key', () => {
expect(
config.TEST_ONLY.serviceNameFromAccessKey('ss://anything#My%20Server')
).toEqual('My Server');
describe('parseAccessKey', () => {
method_channel.installDefaultMethodChannel({
async invokeMethod(methodName: string, params: string): Promise<string> {
if (!params) {
throw Error('empty transport config');
}
if (params.indexOf('invalid') > -1) {
throw Error('fake invalid config');
}
return 'first-hop:4321';
},
});
it('extracts name from ssconf:// key', () => {
expect(
config.TEST_ONLY.serviceNameFromAccessKey('ssconf://anything#My%20Server')
).toEqual('My Server');

it('extracts name from ss:// key', async () => {
const transportConfig = `ss://${encodeURIComponent(
btoa('chacha20-ietf-poly1305:SECRET')
)}@example.com:4321`;
const accessKey = `${transportConfig}#My%20Server`;
expect(await config.TEST_ONLY.parseAccessKey(accessKey)).toEqual(
new config.StaticServiceConfig('My Server', {
firstHop: 'first-hop:4321',
transport: transportConfig,
})
);
});
it('ignores parameters', () => {

it('extracts name from ssconf:// key', async () => {
expect(
config.TEST_ONLY.serviceNameFromAccessKey(
'ss://anything#foo=bar&My%20Server&baz=boo'
await config.TEST_ONLY.parseAccessKey(
'ssconf://example.com:4321/path#My%20Server'
)
).toEqual(
new config.DynamicServiceConfig(
'My Server',
new URL('https://example.com:4321/path')
)
).toEqual('My Server');
);
});

it('name extraction ignores parameters', async () => {
const transportConfig = 'ss://anything';
const accessKey = `${transportConfig}#foo=bar&My%20Server&baz=boo`;
expect(await config.TEST_ONLY.parseAccessKey(accessKey)).toEqual(
new config.StaticServiceConfig('My Server', {
firstHop: 'first-hop:4321',
transport: transportConfig,
})
);
});
});
*/
113 changes: 22 additions & 91 deletions client/src/www/app/outline_server_repository/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import * as method_channel from '@outline/client/src/www/app/method_channel';
import * as errors from '../../model/errors';

export const TEST_ONLY = {
parseAccessKey: parseAccessKey,
getAddressFromTransportConfig: getAddressFromTransportConfig,
serviceNameFromAccessKey: serviceNameFromAccessKey,
};
Expand All @@ -32,21 +33,15 @@ export type ServiceConfig = StaticServiceConfig | DynamicServiceConfig;
* It's the structured representation of a Static Access Key.
*/
export class StaticServiceConfig {
constructor(
readonly name: string,
readonly tunnelConfig: TunnelConfigJson
) {}
constructor(readonly name: string, readonly tunnelConfig: TunnelConfigJson) {}

Check failure on line 36 in client/src/www/app/outline_server_repository/config.ts

View workflow job for this annotation

GitHub Actions / Lint

Replace `readonly·name:·string,·readonly·tunnelConfig:·TunnelConfigJson` with `⏎····readonly·name:·string,⏎····readonly·tunnelConfig:·TunnelConfigJson⏎··`
}

/**
* DynamicServiceConfig is a ServiceConfig that has the location to fetch the tunnel config.
* It's the structured representation of a Dynamic Access Key.
*/
export class DynamicServiceConfig {
constructor(
readonly name: string,
readonly transportConfigLocation: URL
) {}
constructor(readonly name: string, readonly transportConfigLocation: URL) {}

Check failure on line 44 in client/src/www/app/outline_server_repository/config.ts

View workflow job for this annotation

GitHub Actions / Lint

Replace `readonly·name:·string,·readonly·transportConfigLocation:·URL` with `⏎····readonly·name:·string,⏎····readonly·transportConfigLocation:·URL⏎··`
}

/**
Expand Down Expand Up @@ -105,84 +100,35 @@ export async function parseTunnelConfig(
firstHop: await getAddressFromTransportConfig(tunnelConfigText),
transport: tunnelConfigText,
};

// // const firstHop = validateTunnelConfig(tunnelConfigText)
// // return {firstHop: firstHop, tunnelConfig: tunnelConfigText}
// tunnelConfigText = tunnelConfigText.trim();
// if (tunnelConfigText.startsWith('ss://')) {
// return staticKeyToTunnelConfig(tunnelConfigText);
// }

// const responseJson = JSON.parse(tunnelConfigText);

// if ('error' in responseJson) {
// throw new errors.SessionProviderError(
// responseJson.error.message,
// responseJson.error.details
// );
// }

// // TODO(fortuna): stop converting to the Go format. Let the Go code convert.
// // We don't validate the method because that's already done in the Go code as
// // part of the Dynamic Key connection flow.
// const transport: TransportConfigJson = {
// host: responseJson.server,
// port: responseJson.server_port,
// method: responseJson.method,
// password: responseJson.password,
// };
// if (responseJson.prefix) {
// (transport as {prefix?: string}).prefix = responseJson.prefix;
// }
// return {
// transport,
// firstHop: getAddressFromTransportConfig(transport),
// };
}

/** Parses an access key string into a TunnelConfig object. */
// function staticKeyToTunnelConfig(staticKey: string): TunnelConfigJson {
// const config = SHADOWSOCKS_URI.parse(staticKey);
// if (!isShadowsocksCipherSupported(config.method.data)) {
// throw new errors.ShadowsocksUnsupportedCipher(
// config.method.data || 'unknown'
// );
// }
// const transport: TransportConfigJson = {
// host: config.host.data,
// port: config.port.data,
// method: config.method.data,
// password: config.password.data,
// };
// if (config.extra?.['prefix']) {
// (transport as {prefix?: string}).prefix = config.extra?.['prefix'];
// }
// return {
// transport,
// firstHop: getAddressFromTransportConfig(transport),
// };
// }

export async function parseAccessKey(
accessKey: string
): Promise<ServiceConfig> {
export async function parseAccessKey(accessKeyText: string): Promise<ServiceConfig> {

Check failure on line 105 in client/src/www/app/outline_server_repository/config.ts

View workflow job for this annotation

GitHub Actions / Lint

Replace `accessKeyText:·string` with `⏎··accessKeyText:·string⏎`
try {
accessKey = accessKey.trim();
const accessKeyUrl = new URL(accessKeyText.trim());

// The default service name is extracted from the URL fragment of the access key.
const name = serviceNameFromAccessKey(accessKey);
const name = serviceNameFromAccessKey(accessKeyUrl);
// The hash only encodes service config, not tunnel config or config location.
const noHashAccessKey = new URL(accessKeyUrl);
noHashAccessKey.hash = '';

// Static ss:// keys. It encodes the full service config.
if (accessKey.startsWith('ss://')) {
return new StaticServiceConfig(name, await parseTunnelConfig(accessKey));
if (noHashAccessKey.protocol === 'ss:') {
return new StaticServiceConfig(
name,
await parseTunnelConfig(noHashAccessKey.toString())
);
}

// Dynamic ssconf:// keys. It encodes the location of the service config.
if (accessKey.startsWith('ssconf://') || accessKey.startsWith('https://')) {
if (
noHashAccessKey.protocol === 'ssconf:' ||
noHashAccessKey.protocol === 'https:'
) {
try {
// URL does not parse the hostname (treats as opaque string) if the protocol is non-standard (e.g. non-http).
const configLocation = new URL(
accessKey.replace(/^ssconf:\/\//, 'https://')
noHashAccessKey.toString().replace(/^ssconf:\/\//, 'https://')
);
return new DynamicServiceConfig(name, configLocation);
} catch (error) {
Expand All @@ -202,32 +148,17 @@ export function validateAccessKey(accessKey: string) {
getAddressFromTransportConfig(accessKey);
}

// // We only support AEAD ciphers for Shadowsocks.
// // See https://shadowsocks.org/en/spec/AEAD-Ciphers.html
// const SUPPORTED_SHADOWSOCKS_CIPHERS = [
// 'chacha20-ietf-poly1305',
// 'aes-128-gcm',
// 'aes-192-gcm',
// 'aes-256-gcm',
// ];

// function isShadowsocksCipherSupported(cipher?: string): boolean {
// return SUPPORTED_SHADOWSOCKS_CIPHERS.includes(cipher);
// }

/**
* serviceNameFromAccessKey extracts the service name from the access key.
* This is done by getting parsing the fragment hash in the URL and returning the
* entry that is not a key=value pair.
* This is used to name the service card in the UI when the service is added.
*/
function serviceNameFromAccessKey(accessKey: string): string | undefined {
const {hash} = new URL(accessKey.replace(/^ss(?:conf)?:\/\//, 'https://'));

if (!hash) return;
function serviceNameFromAccessKey(accessKey: URL): string | undefined {
if (!accessKey.hash) return;

return decodeURIComponent(
hash
accessKey.hash
.slice(1)
.split('&')
.find(keyValuePair => !keyValuePair.includes('='))
Expand Down
Loading

0 comments on commit cde35c5

Please sign in to comment.