| [62b2964] | 1 | import { SocketOptions, Socket, TlsOptions } from 'cloudflare:sockets'
|
|---|
| 2 | import { EventEmitter } from 'events'
|
|---|
| 3 |
|
|---|
| 4 | /**
|
|---|
| 5 | * Wrapper around the Cloudflare built-in socket that can be used by the `Connection`.
|
|---|
| 6 | */
|
|---|
| 7 | export class CloudflareSocket extends EventEmitter {
|
|---|
| 8 | writable = false
|
|---|
| 9 | destroyed = false
|
|---|
| 10 |
|
|---|
| 11 | private _upgrading = false
|
|---|
| 12 | private _upgraded = false
|
|---|
| 13 | private _cfSocket: Socket | null = null
|
|---|
| 14 | private _cfWriter: WritableStreamDefaultWriter | null = null
|
|---|
| 15 | private _cfReader: ReadableStreamDefaultReader | null = null
|
|---|
| 16 |
|
|---|
| 17 | constructor(readonly ssl: boolean) {
|
|---|
| 18 | super()
|
|---|
| 19 | }
|
|---|
| 20 |
|
|---|
| 21 | setNoDelay() {
|
|---|
| 22 | return this
|
|---|
| 23 | }
|
|---|
| 24 | setKeepAlive() {
|
|---|
| 25 | return this
|
|---|
| 26 | }
|
|---|
| 27 | ref() {
|
|---|
| 28 | return this
|
|---|
| 29 | }
|
|---|
| 30 | unref() {
|
|---|
| 31 | return this
|
|---|
| 32 | }
|
|---|
| 33 |
|
|---|
| 34 | async connect(port: number, host: string, connectListener?: (...args: unknown[]) => void) {
|
|---|
| 35 | try {
|
|---|
| 36 | log('connecting')
|
|---|
| 37 | if (connectListener) this.once('connect', connectListener)
|
|---|
| 38 |
|
|---|
| 39 | const options: SocketOptions = this.ssl ? { secureTransport: 'starttls' } : {}
|
|---|
| 40 | const mod = await import('cloudflare:sockets')
|
|---|
| 41 | const connect = mod.connect
|
|---|
| 42 | this._cfSocket = connect(`${host}:${port}`, options)
|
|---|
| 43 | this._cfWriter = this._cfSocket.writable.getWriter()
|
|---|
| 44 | this._addClosedHandler()
|
|---|
| 45 |
|
|---|
| 46 | this._cfReader = this._cfSocket.readable.getReader()
|
|---|
| 47 | if (this.ssl) {
|
|---|
| 48 | this._listenOnce().catch((e) => this.emit('error', e))
|
|---|
| 49 | } else {
|
|---|
| 50 | this._listen().catch((e) => this.emit('error', e))
|
|---|
| 51 | }
|
|---|
| 52 |
|
|---|
| 53 | await this._cfWriter!.ready
|
|---|
| 54 | log('socket ready')
|
|---|
| 55 | this.writable = true
|
|---|
| 56 | this.emit('connect')
|
|---|
| 57 |
|
|---|
| 58 | return this
|
|---|
| 59 | } catch (e) {
|
|---|
| 60 | this.emit('error', e)
|
|---|
| 61 | }
|
|---|
| 62 | }
|
|---|
| 63 |
|
|---|
| 64 | async _listen() {
|
|---|
| 65 | // eslint-disable-next-line no-constant-condition
|
|---|
| 66 | while (true) {
|
|---|
| 67 | log('awaiting receive from CF socket')
|
|---|
| 68 | const { done, value } = await this._cfReader!.read()
|
|---|
| 69 | log('CF socket received:', done, value)
|
|---|
| 70 | if (done) {
|
|---|
| 71 | log('done')
|
|---|
| 72 | break
|
|---|
| 73 | }
|
|---|
| 74 | this.emit('data', Buffer.from(value))
|
|---|
| 75 | }
|
|---|
| 76 | }
|
|---|
| 77 |
|
|---|
| 78 | async _listenOnce() {
|
|---|
| 79 | log('awaiting first receive from CF socket')
|
|---|
| 80 | const { done, value } = await this._cfReader!.read()
|
|---|
| 81 | log('First CF socket received:', done, value)
|
|---|
| 82 | this.emit('data', Buffer.from(value))
|
|---|
| 83 | }
|
|---|
| 84 |
|
|---|
| 85 | write(
|
|---|
| 86 | data: Uint8Array | string,
|
|---|
| 87 | encoding: BufferEncoding = 'utf8',
|
|---|
| 88 | callback: (...args: unknown[]) => void = () => {}
|
|---|
| 89 | ) {
|
|---|
| 90 | if (data.length === 0) return callback()
|
|---|
| 91 | if (typeof data === 'string') data = Buffer.from(data, encoding)
|
|---|
| 92 |
|
|---|
| 93 | log('sending data direct:', data)
|
|---|
| 94 | this._cfWriter!.write(data).then(
|
|---|
| 95 | () => {
|
|---|
| 96 | log('data sent')
|
|---|
| 97 | callback()
|
|---|
| 98 | },
|
|---|
| 99 | (err) => {
|
|---|
| 100 | log('send error', err)
|
|---|
| 101 | callback(err)
|
|---|
| 102 | }
|
|---|
| 103 | )
|
|---|
| 104 | return true
|
|---|
| 105 | }
|
|---|
| 106 |
|
|---|
| 107 | end(data = Buffer.alloc(0), encoding: BufferEncoding = 'utf8', callback: (...args: unknown[]) => void = () => {}) {
|
|---|
| 108 | log('ending CF socket')
|
|---|
| 109 | this.write(data, encoding, (err) => {
|
|---|
| 110 | this._cfSocket!.close()
|
|---|
| 111 | if (callback) callback(err)
|
|---|
| 112 | })
|
|---|
| 113 | return this
|
|---|
| 114 | }
|
|---|
| 115 |
|
|---|
| 116 | destroy(reason: string) {
|
|---|
| 117 | log('destroying CF socket', reason)
|
|---|
| 118 | this.destroyed = true
|
|---|
| 119 | return this.end()
|
|---|
| 120 | }
|
|---|
| 121 |
|
|---|
| 122 | startTls(options: TlsOptions) {
|
|---|
| 123 | if (this._upgraded) {
|
|---|
| 124 | // Don't try to upgrade again.
|
|---|
| 125 | this.emit('error', 'Cannot call `startTls()` more than once on a socket')
|
|---|
| 126 | return
|
|---|
| 127 | }
|
|---|
| 128 | this._cfWriter!.releaseLock()
|
|---|
| 129 | this._cfReader!.releaseLock()
|
|---|
| 130 | this._upgrading = true
|
|---|
| 131 | this._cfSocket = this._cfSocket!.startTls(options)
|
|---|
| 132 | this._cfWriter = this._cfSocket.writable.getWriter()
|
|---|
| 133 | this._cfReader = this._cfSocket.readable.getReader()
|
|---|
| 134 | this._addClosedHandler()
|
|---|
| 135 | this._listen().catch((e) => this.emit('error', e))
|
|---|
| 136 | }
|
|---|
| 137 |
|
|---|
| 138 | _addClosedHandler() {
|
|---|
| 139 | this._cfSocket!.closed.then(() => {
|
|---|
| 140 | if (!this._upgrading) {
|
|---|
| 141 | log('CF socket closed')
|
|---|
| 142 | this._cfSocket = null
|
|---|
| 143 | this.emit('close')
|
|---|
| 144 | } else {
|
|---|
| 145 | this._upgrading = false
|
|---|
| 146 | this._upgraded = true
|
|---|
| 147 | }
|
|---|
| 148 | }).catch((e) => this.emit('error', e))
|
|---|
| 149 | }
|
|---|
| 150 | }
|
|---|
| 151 |
|
|---|
| 152 | const debug = false
|
|---|
| 153 |
|
|---|
| 154 | function dump(data: unknown) {
|
|---|
| 155 | if (data instanceof Uint8Array || data instanceof ArrayBuffer) {
|
|---|
| 156 | // workaround https://github.com/microsoft/TypeScript/issues/63447
|
|---|
| 157 | const buf = data instanceof Uint8Array ? Buffer.from(data) : Buffer.from(data)
|
|---|
| 158 |
|
|---|
| 159 | const hex = buf.toString('hex')
|
|---|
| 160 | const str = new TextDecoder().decode(data)
|
|---|
| 161 | return `\n>>> STR: "${str.replace(/\n/g, '\\n')}"\n>>> HEX: ${hex}\n`
|
|---|
| 162 | } else {
|
|---|
| 163 | return data
|
|---|
| 164 | }
|
|---|
| 165 | }
|
|---|
| 166 |
|
|---|
| 167 | function log(...args: unknown[]) {
|
|---|
| 168 | debug && console.log(...args.map(dump))
|
|---|
| 169 | }
|
|---|