-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
6 changed files
with
287 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
.DS_Store | ||
tsconfig.tsbuildinfo |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
{ | ||
"name": "@cordisjs/server", | ||
"description": "Server plugin for cordis", | ||
"version": "0.1.2", | ||
"main": "lib/index.js", | ||
"types": "lib/index.d.ts", | ||
"files": [ | ||
"lib", | ||
"src" | ||
], | ||
"author": "Shigma <[email protected]>", | ||
"license": "MIT", | ||
"repository": { | ||
"type": "git", | ||
"url": "git+https://github.com/cordisjs/server.git", | ||
"directory": "packages/core" | ||
}, | ||
"bugs": { | ||
"url": "https://github.com/cordisjs/server/issues" | ||
}, | ||
"homepage": "https://github.com/cordisjs/server", | ||
"keywords": [ | ||
"cordis", | ||
"router", | ||
"http", | ||
"ws", | ||
"websocket", | ||
"server", | ||
"service", | ||
"plugin" | ||
], | ||
"devDependencies": { | ||
"@types/parseurl": "^1.3.3" | ||
}, | ||
"dependencies": { | ||
"@koa/router": "^10.1.1", | ||
"@types/koa": "*", | ||
"@types/koa__router": "*", | ||
"@types/ws": "^8.5.10", | ||
"koa": "^2.14.2", | ||
"koa-bodyparser": "^4.4.1", | ||
"parseurl": "^1.3.3", | ||
"path-to-regexp": "^6.2.1", | ||
"reggol": "^1.6.3", | ||
"schemastery": "^3.14.1", | ||
"ws": "^8.14.2" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,175 @@ | ||
import { Context } from 'cordis' | ||
import { MaybeArray, remove, trimSlash } from 'cosmokit' | ||
import { createServer, IncomingMessage, Server } from 'http' | ||
import { pathToRegexp } from 'path-to-regexp' | ||
import parseUrl from 'parseurl' | ||
import WebSocket from 'ws' | ||
import Logger from 'reggol' | ||
import Schema from 'schemastery' | ||
import KoaRouter from '@koa/router' | ||
import Koa from 'koa' | ||
import { listen } from './listen' | ||
|
||
declare module 'koa' { | ||
// koa-bodyparser | ||
interface Request { | ||
body?: any | ||
rawBody?: string | ||
} | ||
} | ||
|
||
declare module 'cordis' { | ||
interface Context { | ||
router: Router | ||
} | ||
|
||
interface Events { | ||
'router/ready'(this: Router): void | ||
} | ||
} | ||
|
||
type WebSocketCallback = (socket: WebSocket, request: IncomingMessage) => void | ||
|
||
export class WebSocketLayer { | ||
clients = new Set<WebSocket>() | ||
regexp: RegExp | ||
|
||
constructor(private router: Router, path: MaybeArray<string | RegExp>, public callback?: WebSocketCallback) { | ||
this.regexp = pathToRegexp(path) | ||
} | ||
|
||
accept(socket: WebSocket, request: IncomingMessage) { | ||
if (!this.regexp.test(parseUrl(request)!.pathname!)) return | ||
this.clients.add(socket) | ||
socket.addEventListener('close', () => { | ||
this.clients.delete(socket) | ||
}) | ||
this.callback?.(socket, request) | ||
return true | ||
} | ||
|
||
close() { | ||
remove(this.router.wsStack, this) | ||
for (const socket of this.clients) { | ||
socket.close() | ||
} | ||
} | ||
} | ||
|
||
export class Router extends KoaRouter { | ||
public _http: Server | ||
public _ws: WebSocket.Server | ||
public wsStack: WebSocketLayer[] = [] | ||
|
||
public host!: string | ||
public port!: number | ||
|
||
private logger: Logger | ||
|
||
constructor(protected ctx: Context, public config: Router.Config) { | ||
super() | ||
this.logger = new Logger('router', { [Context.current]: this }) | ||
|
||
// create server | ||
const koa = new Koa() | ||
koa.use(require('koa-bodyparser')({ | ||
enableTypes: ['json', 'form', 'xml'], | ||
jsonLimit: '10mb', | ||
formLimit: '10mb', | ||
textLimit: '10mb', | ||
xmlLimit: '10mb', | ||
})) | ||
koa.use(this.routes()) | ||
koa.use(this.allowedMethods()) | ||
|
||
this._http = createServer(koa.callback()) | ||
this._ws = new WebSocket.Server({ | ||
server: this._http, | ||
}) | ||
|
||
this._ws.on('connection', (socket, request) => { | ||
for (const manager of this.wsStack) { | ||
if (manager.accept(socket, request)) return | ||
} | ||
socket.close() | ||
}) | ||
|
||
ctx.decline(['selfUrl', 'host', 'port', 'maxPort']) | ||
|
||
if (config.selfUrl) { | ||
config.selfUrl = trimSlash(config.selfUrl) | ||
} | ||
|
||
ctx.on('ready', async () => { | ||
const { host = '127.0.0.1', port } = config | ||
if (!port) return | ||
this.host = host | ||
this.port = await listen(config) | ||
this._http.listen(this.port, host) | ||
this.logger.info('server listening at %c', this.selfUrl) | ||
ctx.emit(this, 'router/ready') | ||
}, true) | ||
|
||
ctx.on('dispose', () => { | ||
if (config.port) { | ||
this.logger.info('http server closing') | ||
} | ||
this._ws?.close() | ||
this._http?.close() | ||
}) | ||
|
||
ctx.on<any>('event/router/ready', (ctx: Context, listener: Function) => { | ||
if (!this[Context.filter](ctx) || !this.port) return | ||
ctx.scope.ensure(async () => listener()) | ||
return () => false | ||
}) | ||
} | ||
|
||
[Context.filter](ctx: Context) { | ||
return ctx[Context.shadow].router === this.ctx[Context.shadow].router | ||
} | ||
|
||
get selfUrl() { | ||
const wildcard = ['0.0.0.0', '::'] | ||
const host = wildcard.includes(this.host) ? '127.0.0.1' : this.host | ||
return `http://${host}:${this.port}` | ||
} | ||
|
||
/** | ||
* hack into router methods to make sure that koa middlewares are disposable | ||
*/ | ||
register(...args: Parameters<KoaRouter['register']>) { | ||
const layer = super.register(...args) | ||
const context = this[Context.current] | ||
context?.state.disposables.push(() => { | ||
remove(this.stack, layer) | ||
}) | ||
return layer | ||
} | ||
|
||
ws(path: MaybeArray<string | RegExp>, callback?: WebSocketCallback) { | ||
const layer = new WebSocketLayer(this, path, callback) | ||
this.wsStack.push(layer) | ||
const context = this[Context.current] | ||
context?.state.disposables.push(() => layer.close()) | ||
return layer | ||
} | ||
} | ||
|
||
export namespace Router { | ||
export interface Config { | ||
host: string | ||
port: number | ||
maxPort?: number | ||
selfUrl?: string | ||
} | ||
|
||
export const Config: Schema<Config> = Schema.object({ | ||
host: Schema.string().default('127.0.0.1').description('要监听的 IP 地址。如果将此设置为 `0.0.0.0` 将监听所有地址,包括局域网和公网地址。'), | ||
port: Schema.natural().max(65535).description('要监听的初始端口号。'), | ||
maxPort: Schema.natural().max(65535).description('允许监听的最大端口号。'), | ||
selfUrl: Schema.string().role('link').description('应用暴露在公网的地址。'), | ||
}) | ||
} | ||
|
||
export default Router |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
import net from 'net' | ||
|
||
export interface ListenOptions { | ||
host: string | ||
port: number | ||
maxPort?: number | ||
} | ||
|
||
export function listen({ host, port, maxPort = port }: ListenOptions) { | ||
const server = net.createServer() | ||
|
||
return new Promise<number>((resolve, reject) => { | ||
function onListen() { | ||
server.off('error', onError) | ||
server.close((err) => { | ||
err ? reject(err) : resolve(port) | ||
}) | ||
} | ||
|
||
function onError(err: NodeJS.ErrnoException) { | ||
server.off('listening', onListen) | ||
if (!(err.code === 'EADDRINUSE' || err.code === 'EACCES')) { | ||
return reject(err) | ||
} | ||
port++ | ||
if (port > maxPort) { | ||
return reject(new Error('No open ports available')) | ||
} | ||
testPort() | ||
} | ||
|
||
function testPort() { | ||
server.once('error', onError) | ||
server.once('listening', onListen) | ||
server.listen(port, host) | ||
} | ||
|
||
testPort() | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
{ | ||
"extends": "../../tsconfig.base", | ||
"compilerOptions": { | ||
"rootDir": "src", | ||
"outDir": "lib", | ||
"strict": true, | ||
"noImplicitAny": false, | ||
}, | ||
"include": [ | ||
"src", | ||
], | ||
} |