-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Cloudflare Workers support #2971
Merged
Merged
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
8eaf125
Add local development helper doc
petebacondarwin c208547
fix invalid connection string test
petebacondarwin eeb1779
Use `URL` rather than `url.parse()` in pg-connection-string
petebacondarwin e5b49c8
avoid accessing Node specific requires when not needed
petebacondarwin b79f5ff
Use WebCrypto APIs where possible
petebacondarwin 47d2d54
Add Cloudflare Worker compatible socket
petebacondarwin 2813660
Add example Cloudflare Worker and test
petebacondarwin 8fcd6bd
Clean up pg-native in Makefile better
petebacondarwin File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
# Local development | ||
|
||
Steps to install and configure Postgres on Mac for developing against locally | ||
|
||
1. Install homebrew | ||
2. Install postgres | ||
```sh | ||
brew install postgresql | ||
``` | ||
3. Create a database | ||
```sh | ||
createdb test | ||
``` | ||
4. Create SSL certificates | ||
```sh | ||
cd /opt/homebrew/var/postgresql@14 | ||
openssl genrsa -aes128 2048 > server.key | ||
openssl rsa -in server.key -out server.key | ||
chmod 400 server.key | ||
openssl req -new -key server.key -days 365 -out server.crt -x509 | ||
cp server.crt root.crt | ||
``` | ||
5. Update config in `/opt/homebrew/var/postgresql@14/postgresql.conf` | ||
|
||
```conf | ||
listen_addresses = '*' | ||
|
||
password_encryption = md5 | ||
|
||
ssl = on | ||
ssl_ca_file = 'root.crt' | ||
ssl_cert_file = 'server.crt' | ||
ssl_crl_file = '' | ||
ssl_crl_dir = '' | ||
ssl_key_file = 'server.key' | ||
ssl_ciphers = 'HIGH:MEDIUM:+3DES:!aNULL' # allowed SSL ciphers | ||
ssl_prefer_server_ciphers = on | ||
``` | ||
|
||
6. Start Postgres server | ||
```sh | ||
/opt/homebrew/opt/postgresql@14/bin/postgres -D /opt/homebrew/var/postgresql@14 | ||
``` |
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,33 @@ | ||
# pg-cloudflare | ||
|
||
A socket implementation that can run on Cloudflare Workers using native TCP connections. | ||
|
||
## install | ||
|
||
``` | ||
npm i --save-dev pg-cloudflare | ||
``` | ||
|
||
### license | ||
|
||
The MIT License (MIT) | ||
|
||
Copyright (c) 2023 Brian M. Carlson | ||
|
||
Permission is hereby granted, free of charge, to any person obtaining a copy | ||
of this software and associated documentation files (the "Software"), to deal | ||
in the Software without restriction, including without limitation the rights | ||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
copies of the Software, and to permit persons to whom the Software is | ||
furnished to do so, subject to the following conditions: | ||
|
||
The above copyright notice and this permission notice shall be included in | ||
all copies or substantial portions of the Software. | ||
|
||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | ||
THE SOFTWARE. |
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,27 @@ | ||
{ | ||
"name": "pg-cloudflare", | ||
"version": "1.0.0", | ||
"description": "A socket implementation that can run on Cloudflare Workers using native TCP connections.", | ||
"main": "dist/index.js", | ||
"types": "dist/index.d.ts", | ||
"license": "MIT", | ||
"devDependencies": { | ||
"ts-node": "^8.5.4", | ||
"typescript": "^4.0.3" | ||
}, | ||
"scripts": { | ||
"build": "tsc", | ||
"build:watch": "tsc --watch", | ||
"prepublish": "yarn build", | ||
"test": "echo e2e test in pg package" | ||
}, | ||
"repository": { | ||
"type": "git", | ||
"url": "git://github.com/brianc/node-postgres.git", | ||
"directory": "packages/pg-cloudflare" | ||
}, | ||
"files": [ | ||
"/dist/*{js,ts,map}", | ||
"/src" | ||
] | ||
} |
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,164 @@ | ||
import { SocketOptions, Socket, TlsOptions } from 'cloudflare:sockets' | ||
import { EventEmitter } from 'events' | ||
|
||
/** | ||
* Wrapper around the Cloudflare built-in socket that can be used by the `Connection`. | ||
*/ | ||
export class CloudflareSocket extends EventEmitter { | ||
writable = false | ||
destroyed = false | ||
|
||
private _upgrading = false | ||
private _upgraded = false | ||
private _cfSocket: Socket | null = null | ||
private _cfWriter: WritableStreamDefaultWriter | null = null | ||
private _cfReader: ReadableStreamDefaultReader | null = null | ||
|
||
constructor(readonly ssl: boolean) { | ||
super() | ||
} | ||
|
||
setNoDelay() { | ||
return this | ||
} | ||
setKeepAlive() { | ||
return this | ||
} | ||
ref() { | ||
return this | ||
} | ||
unref() { | ||
return this | ||
} | ||
|
||
async connect(port: number, host: string, connectListener?: (...args: unknown[]) => void) { | ||
try { | ||
log('connecting') | ||
if (connectListener) this.once('connect', connectListener) | ||
|
||
const options: SocketOptions = this.ssl ? { secureTransport: 'starttls' } : {} | ||
const { connect } = await import('cloudflare:sockets') | ||
this._cfSocket = connect(`${host}:${port}`, options) | ||
this._cfWriter = this._cfSocket.writable.getWriter() | ||
this._addClosedHandler() | ||
|
||
this._cfReader = this._cfSocket.readable.getReader() | ||
if (this.ssl) { | ||
this._listenOnce().catch((e) => this.emit('error', e)) | ||
} else { | ||
this._listen().catch((e) => this.emit('error', e)) | ||
} | ||
|
||
await this._cfWriter!.ready | ||
log('socket ready') | ||
this.writable = true | ||
this.emit('connect') | ||
|
||
return this | ||
} catch (e) { | ||
this.emit('error', e) | ||
} | ||
} | ||
|
||
async _listen() { | ||
while (true) { | ||
log('awaiting receive from CF socket') | ||
const { done, value } = await this._cfReader!.read() | ||
log('CF socket received:', done, value) | ||
if (done) { | ||
log('done') | ||
break | ||
} | ||
this.emit('data', Buffer.from(value)) | ||
} | ||
} | ||
|
||
async _listenOnce() { | ||
log('awaiting first receive from CF socket') | ||
const { done, value } = await this._cfReader!.read() | ||
log('First CF socket received:', done, value) | ||
this.emit('data', Buffer.from(value)) | ||
} | ||
|
||
write( | ||
data: Uint8Array | string, | ||
encoding: BufferEncoding = 'utf8', | ||
callback: (...args: unknown[]) => void = () => {} | ||
) { | ||
if (data.length === 0) return callback() | ||
if (typeof data === 'string') data = Buffer.from(data, encoding) | ||
|
||
log('sending data direct:', data) | ||
this._cfWriter!.write(data).then( | ||
() => { | ||
log('data sent') | ||
callback() | ||
}, | ||
(err) => { | ||
log('send error', err) | ||
callback(err) | ||
} | ||
) | ||
return true | ||
} | ||
|
||
end(data = Buffer.alloc(0), encoding: BufferEncoding = 'utf8', callback: (...args: unknown[]) => void = () => {}) { | ||
log('ending CF socket') | ||
this.write(data, encoding, (err) => { | ||
this._cfSocket!.close() | ||
if (callback) callback(err) | ||
}) | ||
return this | ||
} | ||
|
||
destroy(reason: string) { | ||
log('destroying CF socket', reason) | ||
this.destroyed = true | ||
return this.end() | ||
} | ||
|
||
startTls(options: TlsOptions) { | ||
if (this._upgraded) { | ||
// Don't try to upgrade again. | ||
this.emit('error', 'Cannot call `startTls()` more than once on a socket') | ||
return | ||
} | ||
this._cfWriter!.releaseLock() | ||
this._cfReader!.releaseLock() | ||
this._upgrading = true | ||
this._cfSocket = this._cfSocket!.startTls(options) | ||
this._cfWriter = this._cfSocket.writable.getWriter() | ||
this._cfReader = this._cfSocket.readable.getReader() | ||
this._addClosedHandler() | ||
this._listen().catch((e) => this.emit('error', e)) | ||
} | ||
|
||
_addClosedHandler() { | ||
this._cfSocket!.closed.then(() => { | ||
if (!this._upgrading) { | ||
log('CF socket closed') | ||
this._cfSocket = null | ||
this.emit('close') | ||
} else { | ||
this._upgrading = false | ||
this._upgraded = true | ||
} | ||
}).catch((e) => this.emit('error', e)) | ||
} | ||
} | ||
|
||
const debug = false | ||
|
||
function dump(data: unknown) { | ||
if (data instanceof Uint8Array || data instanceof ArrayBuffer) { | ||
const hex = Buffer.from(data).toString('hex') | ||
const str = new TextDecoder().decode(data) | ||
return `\n>>> STR: "${str.replace(/\n/g, '\\n')}"\n>>> HEX: ${hex}\n` | ||
} else { | ||
return data | ||
} | ||
} | ||
|
||
function log(...args: unknown[]) { | ||
debug && console.log(...args.map(dump)) | ||
} | ||
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,25 @@ | ||
declare module 'cloudflare:sockets' { | ||
export class Socket { | ||
public readonly readable: any | ||
public readonly writable: any | ||
public readonly closed: Promise<void> | ||
public close(): Promise<void> | ||
public startTls(options: TlsOptions): Socket | ||
} | ||
|
||
export type TlsOptions = { | ||
expectedServerHostname?: string | ||
} | ||
|
||
export type SocketAddress = { | ||
hostname: string | ||
port: number | ||
} | ||
|
||
export type SocketOptions = { | ||
secureTransport?: 'off' | 'on' | 'starttls' | ||
allowHalfOpen?: boolean | ||
} | ||
|
||
export function connect(address: string | SocketAddress, options?: SocketOptions): Socket | ||
} |
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,25 @@ | ||
{ | ||
"compilerOptions": { | ||
"module": "ES2020", | ||
"esModuleInterop": true, | ||
"allowSyntheticDefaultImports": true, | ||
"strict": true, | ||
"target": "ES2020", | ||
"noImplicitAny": true, | ||
"moduleResolution": "node", | ||
"sourceMap": true, | ||
"outDir": "dist", | ||
"incremental": true, | ||
"baseUrl": ".", | ||
"declaration": true, | ||
"paths": { | ||
"*": [ | ||
"node_modules/*", | ||
"src/types/*" | ||
] | ||
} | ||
}, | ||
"include": [ | ||
"src/**/*" | ||
] | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Leftovers?