Skip to content

Commit

Permalink
fix(sockets/SocketClient): 允许 Socket 地址带有 pathname 参数
Browse files Browse the repository at this point in the history
...并提取出从 socketUrl 获取 host 和 path 的逻辑,另外,兼容以
`/websocket` 结尾的 socketUrl。
  • Loading branch information
aicest authored and chuan6 committed Jan 31, 2019
1 parent 123b9cd commit 9f177e7
Show file tree
Hide file tree
Showing 6 changed files with 200 additions and 4 deletions.
2 changes: 1 addition & 1 deletion mock/mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ export function reParseQuery(uri: string): string {
return uri
}

const context = typeof window !== 'undefined' ? window : global
export const context = typeof window !== 'undefined' ? window : global

const originFetch = context['fetch']

Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
"@types/sinon": "^4.0.0",
"@types/sinon-chai": "^2.7.29",
"@types/uuid": "^3.4.3",
"atob": "^2.1.2",
"chai": "^4.1.2",
"coveralls": "^3.0.0",
"engine.io-client": "^3.1.0",
Expand All @@ -63,6 +64,7 @@
"isomorphic-fetch": "^2.2.1",
"jsonrpc-lite": "^1.3.0",
"madge": "^3.3.0",
"mock-require": "^3.0.3",
"moment": "^2.18.1",
"node-watch": "^0.5.8",
"nyc": "^11.2.1",
Expand Down
35 changes: 33 additions & 2 deletions src/sockets/SocketClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import 'rxjs/add/operator/catch'
import 'rxjs/add/operator/toPromise'
import 'rxjs/add/operator/concatMap'
import 'rxjs/add/operator/take'
import * as URL from 'url'
import * as PATH from 'path'
import { Observable } from 'rxjs/Observable'
import { ReplaySubject } from 'rxjs/ReplaySubject'
import { Net } from '../Net'
Expand All @@ -22,6 +24,23 @@ declare const global: any

const ctx = typeof global === 'undefined' ? window : global

const socketUrlToHostPath = (socketUrl: string) => {
const postfix = '/websocket'

const url = URL.parse(socketUrl)
const host = URL.format({ ...url, pathname: undefined })
// 由于 `socketUrl`
// 1. 若是 http 协议,那么 `pathname` 有值,至少是 `/`;
// 2. 若是 ws 协议,那么 `pathname` 可能没有值;
// 因此,这里使用 PATH 包来完善处理路径结合的逻辑,生成正确 `path`。
let path = url.pathname || ''
if (path.slice(-postfix.length) !== postfix) {
path = PATH.join(path, postfix)
}

return { host, path }
}

export class SocketClient {
private _isDebug = false

Expand Down Expand Up @@ -78,6 +97,17 @@ export class SocketClient {
ctx['console']['log']('socket debug start')
}

/**
* 常见的 url 参数有(它们的值一般写在配置文件里)
* - `https://messaging.__domain__`(如公有云部署)
* - `wss://__host__/messaging`(如私有云部署)
*
* 注意:`url` 参数并不是最终被用来与服务器端链接的 url;目前 teambition
* 服务端要求 websocket 链接的目标 url 以 `/websocket` 结尾,
* 所以参数里提供的 url 如果不是以 `/websocket` 结尾(一般配置都会
* 省略这个路径片段),请求时会在 url 的路径上加上 `websocket` 片段,
* 如:`https://messaging.teambition.net/websocket`。
*/
setSocketUrl(url: string): void {
this._socketUrl = url
}
Expand Down Expand Up @@ -165,14 +195,15 @@ export class SocketClient {
}

private _connect(): Promise<void> {
const { host, path } = socketUrlToHostPath(this._socketUrl)
return this._getUserMeStream
.take(1)
.toPromise()
.then(userMe => {
if (this._client) {
this._client
.connect(this._socketUrl, {
path: '/websocket',
.connect(host, {
path,
token: userMe.tcmToken as string
})
} else {
Expand Down
144 changes: 144 additions & 0 deletions test/sockets/SocketClient.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import { describe, it, beforeEach, afterEach } from 'tman'
import { expect } from 'chai'
import * as sinon from 'sinon'
import * as moment from 'moment'
import * as URL from 'url'

import { createSdkWithoutRDB, context, SDK, UserMe } from '..'

const mockRequire = require('mock-require')

// atob polyfill
// 这个方法在 浏览器 环境才存在
const atob = require('atob')
context.atob = context.atob || atob

describe('SocketClient', () => {
const userMe = { tcmToken: 'mock-tcm-token' } as UserMe

let sdk: SDK
let sandbox: sinon.SinonSandbox
let fetchSpy: sinon.SinonSpy

beforeEach(() => {
sandbox = sinon.createSandbox()

// https://github.com/socketio/engine.io-client/blob/master/lib/transports/polling-xhr.js#L7
mockRequire(
'xmlhttprequest-ssl',
class XMLHttpRequest {
open = (fetchSpy = sandbox.spy())
send = sandbox.spy()
}
)

sdk = createSdkWithoutRDB()
const Consumer = require('snapper-consumer')
sdk.socketClient.initClient(new Consumer(), userMe)

// 用于解析 tcmToken 过期时间
sandbox.stub(context, 'atob').value(() => {
return JSON.stringify({
// 返回一个较晚的过期时间(一年后)
exp: moment()
.add(1, 'y')
.valueOf()
})
})
})

afterEach(() => {
sandbox.restore()
mockRequire.stopAll()
})

it('should connect to target URL (HTTP)', async () => {
const socketUrl = 'http://localhost:1111'
const url = `${socketUrl}/websocket/?token=${userMe.tcmToken}`

sdk.socketClient.setSocketUrl(socketUrl)
await sdk.socketClient.connect()

expect(fetchSpy.calledWith('GET', sinon.match(url))).to.be.true
})

it('should connect to target URL (WS)', async () => {
const socketUrl = 'ws://localhost:1111'
const url = `${socketUrl}/websocket/?token=${userMe.tcmToken}`

sdk.socketClient.setSocketUrl(socketUrl)
await sdk.socketClient.connect()

expect(
fetchSpy.calledWith(
'GET',
sinon.match((pollingUri: string) => matchPollingUri(pollingUri, url))
)
).to.be.true
})

it('should allow SocketURL (HTTP) including url', async () => {
const socketUrl = 'http://localhost:1111/messaging'
const url = `${socketUrl}/websocket/?token=${userMe.tcmToken}`

sdk.socketClient.setSocketUrl(socketUrl)
await sdk.socketClient.connect()

expect(fetchSpy.calledWith('GET', sinon.match(url))).to.be.true
})

it('should allow SocketURL (WS) including url', async () => {
const socketUrl = 'ws://localhost:1111/messaging'
const url = `${socketUrl}/websocket/?token=${userMe.tcmToken}`

sdk.socketClient.setSocketUrl(socketUrl)
await sdk.socketClient.connect()

expect(
fetchSpy.calledWith(
'GET',
sinon.match((pollingUri: string) => matchPollingUri(pollingUri, url))
)
).to.be.true
})

it('should not append `/websocket` postfix if the SocketURL already has it', async () => {
const socketUrl = 'ws://localhost:1111/messaging/websocket'
const url = `ws://localhost:1111/messaging/websocket/?token=${userMe.tcmToken}`

sdk.socketClient.setSocketUrl(socketUrl)
await sdk.socketClient.connect()

expect(
fetchSpy.calledWith(
'GET',
sinon.match((pollingUri: string) => matchPollingUri(pollingUri, url))
)
).to.be.true
})
})

const matchPollingUri = (actual: string, expected: string) => {
const actualUri = toPollingUri(actual)
const expectedUri = toPollingUri(expected)

return actualUri.includes(expectedUri)
}

// 在 Engine.IO 里会把 ws/wss 转为 http/https
// https://github.com/socketio/engine.io-client/blob/master/lib/transports/polling.js#L218
const toPollingUri = (url: string) => {
const urlObj = URL.parse(url)
const schema =
(urlObj.protocol && PollingUriSchemaMap[urlObj.protocol]) || urlObj.protocol

return URL.format({
...urlObj,
protocol: schema
})
}

const PollingUriSchemaMap: Record<string, string> = {
'ws:': 'http:',
'wss:': 'https:'
}
1 change: 1 addition & 0 deletions test/sockets/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ import './tableAlias.spec'
import './mapToTable.spec'
import './middleware.spec'
import './interceptors.spec'
import './SocketClient.spec'
20 changes: 19 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,11 @@ asynckit@^0.4.0:
resolved "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
integrity sha1-x57Zf380y48robyXkLzDZkdLS3k=

atob@^2.1.2:
version "2.1.2"
resolved "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9"
integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==

aws-sign2@~0.7.0:
version "0.7.0"
resolved "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8"
Expand Down Expand Up @@ -1174,6 +1179,11 @@ get-caller-file@^1.0.1:
resolved "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.2.tgz#f702e63127e7e231c160a80c1554acb70d5047e5"
integrity sha1-9wLmMSfn4jHBYKgMFVSstw1QR+U=

get-caller-file@^1.0.2:
version "1.0.3"
resolved "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz#f978fa4c90d1dfe7ff2d6beda2a515e713bdcf4a"
integrity sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==

get-func-name@^2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz#ead774abee72e20409433a066366023dd6887a41"
Expand Down Expand Up @@ -1972,6 +1982,14 @@ mkdirp@^0.5.0, mkdirp@^0.5.1:
dependencies:
minimist "0.0.8"

mock-require@^3.0.3:
version "3.0.3"
resolved "https://registry.npmjs.org/mock-require/-/mock-require-3.0.3.tgz#ccd544d9eae81dd576b3f219f69ec867318a1946"
integrity sha512-lLzfLHcyc10MKQnNUCv7dMcoY/2Qxd6wJfbqCcVk3LDb8An4hF6ohk5AztrvgKhJCqj36uyzi/p5se+tvyD+Wg==
dependencies:
get-caller-file "^1.0.2"
normalize-path "^2.1.1"

module-definition@^3.0.0, module-definition@^3.1.0:
version "3.1.0"
resolved "https://registry.npmjs.org/module-definition/-/module-definition-3.1.0.tgz#201c062b89f81ed18018e1a2f15afc0c8089a126"
Expand Down Expand Up @@ -2069,7 +2087,7 @@ normalize-package-data@^2.3.2:
semver "2 || 3 || 4 || 5"
validate-npm-package-license "^3.0.1"

normalize-path@^2.0.1:
normalize-path@^2.0.1, normalize-path@^2.1.1:
version "2.1.1"
resolved "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9"
integrity sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=
Expand Down

0 comments on commit 9f177e7

Please sign in to comment.