mirror of
https://github.com/hzrd149/nsite-gateway.git
synced 2025-06-23 20:05:03 +00:00
Compare commits
23 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
364e947245 | ||
![]() |
53a4b9f548 | ||
![]() |
1473eee4fd | ||
![]() |
7fa6a79d3b | ||
![]() |
808ffa77be | ||
![]() |
243fe2cd5a | ||
![]() |
14d767114a | ||
![]() |
9a04f63712 | ||
![]() |
b37664bc5b | ||
![]() |
ef5262f73c | ||
![]() |
c3778507d4 | ||
![]() |
80aab93bb7 | ||
![]() |
225b616a3c | ||
![]() |
638f798df1 | ||
![]() |
d87497e6c0 | ||
![]() |
b2b8e0108e | ||
![]() |
2fc6fbc2f1 | ||
![]() |
8fee897834 | ||
![]() |
023e03ec49 | ||
![]() |
13f5b2ce20 | ||
![]() |
3f218e9765 | ||
![]() |
c4cfa61c76 | ||
![]() |
3747037f89 |
.env.example
.github/workflows
.gitignore.nvmrc.vscode
CHANGELOG.mdCaddyfileDockerfileDockerfile-screenshotsLICENSEREADME.mdcontrib
docker-compose.ymldocker-entrypoint.shnginx
package.jsonpnpm-lock.yamlpublic
src
supervisord.conf
18
.env.example
18
.env.example
@ -2,6 +2,9 @@
|
||||
# can be in-memory, redis:// or sqlite://
|
||||
CACHE_PATH="in-memory"
|
||||
|
||||
# How long to keep cached data (in seconds)
|
||||
CACHE_TIME=3600
|
||||
|
||||
# A list of relays to find users relay lists (10002) and blossom servers (10063)
|
||||
LOOKUP_RELAYS=wss://user.kindpag.es,wss://purplepag.es
|
||||
|
||||
@ -9,13 +12,10 @@ LOOKUP_RELAYS=wss://user.kindpag.es,wss://purplepag.es
|
||||
SUBSCRIPTION_RELAYS=wss://nos.lol,wss://relay.damus.io
|
||||
|
||||
# A list of fallback blossom servers
|
||||
BLOSSOM_SERVERS=https://nostr.download,https://cdn.satellite.earth
|
||||
BLOSSOM_SERVERS="https://nostr.download,https://cdn.satellite.earth"
|
||||
|
||||
# The max file size to serve
|
||||
MAX_FILE_SIZE='2 MB'
|
||||
|
||||
# The cache folder for nginx
|
||||
NGINX_CACHE_DIR='/var/nginx/cache'
|
||||
MAX_FILE_SIZE="2 MB"
|
||||
|
||||
# A nprofile pointer for an nsite to use as the default homepage
|
||||
# Setting this will override anything in the ./public folder
|
||||
@ -24,9 +24,11 @@ NSITE_HOMEPAGE=""
|
||||
# a local directory to download the homepage to
|
||||
NSITE_HOMEPAGE_DIR="public"
|
||||
|
||||
# Screenshots require Puppeteer to be setup https://pptr.dev/troubleshooting#setting-up-chrome-linux-sandbox
|
||||
ENABLE_SCREENSHOTS="false"
|
||||
SCREENSHOTS_DIR="./screenshots"
|
||||
# The public domain of the gateway (optional) (used to detect when to show the nsite homepage)
|
||||
PUBLIC_DOMAIN="nsite.gateway.com"
|
||||
|
||||
# The nip-05 domain to use for name resolution
|
||||
# NIP05_NAME_DOMAINS="example.com,nostr.other.site"
|
||||
|
||||
# If this is set, nsite will return the 'Onion-Location' header in responses
|
||||
# ONION_HOST=https://<hostname>.onion
|
||||
|
37
.github/workflows/publish-next.yml
vendored
37
.github/workflows/publish-next.yml
vendored
@ -1,37 +0,0 @@
|
||||
name: Release next
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Release next
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout Repo
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Setup Node.js 20
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install Dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Publish next version
|
||||
run: |
|
||||
pnpm config set //registry.npmjs.org/:_authToken=$NPM_TOKEN && \
|
||||
pnpm whoami && \
|
||||
pnpm changeset version --snapshot next && \
|
||||
pnpm changeset publish --tag next
|
||||
env:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
4
.github/workflows/version-or-publish.yml
vendored
4
.github/workflows/version-or-publish.yml
vendored
@ -26,10 +26,10 @@ jobs:
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Setup Node.js 20
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
node-version-file: .nvmrc
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install Dependencies
|
||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -3,4 +3,4 @@ build
|
||||
.env
|
||||
data
|
||||
.netrc
|
||||
screenshots
|
||||
|
||||
|
1
.nvmrc
Normal file
1
.nvmrc
Normal file
@ -0,0 +1 @@
|
||||
22
|
3
.vscode/launch.json
vendored
3
.vscode/launch.json
vendored
@ -26,8 +26,7 @@
|
||||
"internalConsoleOptions": "openOnSessionStart",
|
||||
"outputCapture": "std",
|
||||
"env": {
|
||||
"DEBUG": "nsite,nsite:*",
|
||||
"ENABLE_SCREENSHOTS": "true"
|
||||
"DEBUG": "nsite,nsite:*"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
37
CHANGELOG.md
37
CHANGELOG.md
@ -1,4 +1,39 @@
|
||||
# nsite-ts
|
||||
# nsite-gateway
|
||||
|
||||
## 1.0.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 1473eee: Fix returning setup page when event can't be found for pubkey
|
||||
|
||||
## 1.0.0
|
||||
|
||||
### Major Changes
|
||||
|
||||
- ef5262f: Remove screenshots feature
|
||||
- ef5262f: Remove nginx cache invalidations
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- b37664b: Cleanup DNS pubkey resolution
|
||||
- 9a04f63: Add support for resolving NIP-05 names on set domains
|
||||
- b2b8e01: Make blossom requests in parallel
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- ef5262f: Fix race condition when streaming blob
|
||||
|
||||
## 0.7.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 023e03e: Rename package to nsite-gateway
|
||||
|
||||
## 0.6.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 3747037: Add license file
|
||||
|
||||
## 0.6.0
|
||||
|
||||
|
8
Caddyfile
Normal file
8
Caddyfile
Normal file
@ -0,0 +1,8 @@
|
||||
#{
|
||||
# email your-email@example.com
|
||||
#}
|
||||
|
||||
# This will match example.com and all its subdomains (*.example.com)
|
||||
example.com, *.example.com {
|
||||
reverse_proxy nsite:3000
|
||||
}
|
22
Dockerfile
22
Dockerfile
@ -1,13 +1,9 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
FROM node:20-alpine AS base
|
||||
FROM node:22-alpine AS base
|
||||
|
||||
ENV PNPM_HOME="/pnpm"
|
||||
ENV PATH="$PNPM_HOME:$PATH"
|
||||
RUN corepack enable
|
||||
|
||||
RUN apk update && apk add --no-cache nginx supervisor
|
||||
COPY supervisord.conf /etc/supervisord.conf
|
||||
|
||||
WORKDIR /app
|
||||
COPY package.json .
|
||||
COPY pnpm-lock.yaml .
|
||||
@ -27,25 +23,13 @@ FROM base AS main
|
||||
RUN addgroup -S nsite && adduser -S nsite -G nsite
|
||||
RUN chown -R nsite:nsite /app
|
||||
|
||||
# Setup nginx
|
||||
COPY nginx/nginx.conf /etc/nginx/nginx.conf
|
||||
COPY nginx/http.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
# setup nsite
|
||||
COPY --from=prod-deps /app/node_modules /app/node_modules
|
||||
COPY --from=build ./app/build ./build
|
||||
|
||||
COPY ./public ./public
|
||||
|
||||
VOLUME [ "/var/cache/nginx" ]
|
||||
|
||||
EXPOSE 80 3000
|
||||
EXPOSE 3000
|
||||
ENV NSITE_PORT="3000"
|
||||
ENV NGINX_CACHE_DIR="/var/cache/nginx"
|
||||
ENV ENABLE_SCREENSHOTS="false"
|
||||
|
||||
COPY docker-entrypoint.sh /
|
||||
RUN chmod +x /docker-entrypoint.sh
|
||||
ENTRYPOINT ["/docker-entrypoint.sh"]
|
||||
|
||||
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisord.conf"]
|
||||
CMD ["node", "."]
|
||||
|
@ -1,75 +0,0 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
FROM node:20-slim AS base
|
||||
|
||||
ENV PNPM_HOME="/pnpm"
|
||||
ENV PATH="$PNPM_HOME:$PATH"
|
||||
RUN corepack enable
|
||||
|
||||
# Setup nsite user
|
||||
RUN groupadd -r nsite && useradd -r -g nsite -G audio,video nsite && usermod -d /app nsite
|
||||
|
||||
# Install nginx and supervisor
|
||||
RUN apt-get update && apt-get install -y nginx supervisor
|
||||
|
||||
# setup supervisor
|
||||
COPY supervisord.conf /etc/supervisord.conf
|
||||
|
||||
# Setup nginx
|
||||
COPY nginx/nginx.conf /etc/nginx/nginx.conf
|
||||
COPY nginx/http.conf /etc/nginx/conf.d/default.conf
|
||||
RUN chown nsite:nsite -R /etc/nginx
|
||||
|
||||
# install google chrome for screenshots. copied from (https://pptr.dev/troubleshooting#running-puppeteer-in-docker)
|
||||
|
||||
# Install latest chrome dev package and fonts to support major charsets (Chinese, Japanese, Arabic, Hebrew, Thai and a few others)
|
||||
# Note: this installs the necessary libs to make the bundled version of Chrome for Testing that Puppeteer
|
||||
# installs, work.
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y wget gnupg \
|
||||
&& wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \
|
||||
&& sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' \
|
||||
&& apt-get update \
|
||||
&& apt-get install -y google-chrome-stable fonts-ipafont-gothic fonts-wqy-zenhei fonts-thai-tlwg fonts-kacst fonts-freefont-ttf libxss1 \
|
||||
--no-install-recommends \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
COPY package.json .
|
||||
COPY pnpm-lock.yaml .
|
||||
|
||||
FROM base AS prod-deps
|
||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --prod --frozen-lockfile
|
||||
|
||||
FROM base AS build
|
||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
|
||||
COPY tsconfig.json .
|
||||
COPY src ./src
|
||||
RUN pnpm build
|
||||
|
||||
FROM base AS main
|
||||
|
||||
# setup nsite
|
||||
COPY --from=prod-deps /app/node_modules /app/node_modules
|
||||
COPY --from=build ./app/build ./build
|
||||
|
||||
COPY ./public ./public
|
||||
|
||||
VOLUME [ "/var/cache/nginx" ]
|
||||
VOLUME [ "/screenshots" ]
|
||||
|
||||
EXPOSE 80 3000
|
||||
ENV NSITE_PORT="3000"
|
||||
ENV NGINX_CACHE_DIR="/var/cache/nginx"
|
||||
ENV ENABLE_SCREENSHOTS="true"
|
||||
ENV SCREENSHOTS_DIR="/screenshots"
|
||||
ENV PUPPETEER_SKIP_DOWNLOAD="true"
|
||||
|
||||
COPY docker-entrypoint.sh /
|
||||
RUN chmod +x /docker-entrypoint.sh
|
||||
|
||||
# change ownership of app
|
||||
RUN chown nsite:nsite -R /app
|
||||
|
||||
# Run /docker-entrypoint as root so supervisor can run
|
||||
ENTRYPOINT ["/docker-entrypoint.sh"]
|
||||
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisord.conf"]
|
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2025 hzrd149
|
||||
|
||||
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.
|
86
README.md
86
README.md
@ -1,89 +1,39 @@
|
||||
# nsite-ts
|
||||
# nsite-gateway
|
||||
|
||||
A Typescript implementation of [nsite](https://github.com/lez/nsite)
|
||||
A Typescript implementation of [static websites on nostr](https://github.com/nostr-protocol/nips/pull/1538)
|
||||
|
||||
## Running with docker-compose
|
||||
## Configuring
|
||||
|
||||
```sh
|
||||
git clone https://github.com/hzrd149/nsite-ts.git
|
||||
cd nsite-ts
|
||||
docker compose up
|
||||
```
|
||||
|
||||
Once the service is running you can access the cached version at `http://localhost:8080`
|
||||
|
||||
If you need to test, you can directly access the ts server at `http://localhost:3000`
|
||||
|
||||
## Running with docker
|
||||
|
||||
The `ghcr.io/hzrd149/nsite-ts` image can be used to run a http instance locally
|
||||
|
||||
```sh
|
||||
docker run --rm -it --name nsite -p 8080:80 ghcr.io/hzrd149/nsite-ts
|
||||
```
|
||||
|
||||
## Manual nginx setup
|
||||
|
||||
Before manually setting up nginx and nsite-ts you need a few things installed
|
||||
- [nginx](https://nginx.org/)
|
||||
- [nodejs](https://nodejs.org/en/download/package-manager) (dep packages [here](https://deb.nodesource.com/))
|
||||
- [pnpm](https://pnpm.io/) run `npm i -g pnpm` to install
|
||||
|
||||
Next your going to need to clone the nsite-ts repo and set it up
|
||||
|
||||
```sh
|
||||
git clone https://github.com/hzrd149/nsite-ts
|
||||
cd nsite-ts
|
||||
|
||||
# install dependencies
|
||||
pnpm install
|
||||
|
||||
# build app
|
||||
pnpm build
|
||||
```
|
||||
|
||||
Then create a new `.env` file for configuration
|
||||
All configuration is done through the `.env` file. start by copying the example file and modifying it.
|
||||
|
||||
```sh
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
Next copy and setup the systemd service
|
||||
## Running with npx
|
||||
|
||||
```sh
|
||||
sudo cp contrib/nsite.service /etx/systemd/system/nsite.service
|
||||
|
||||
# edit the service and set the working directory path
|
||||
sudo nano /etx/systemd/system/nsite.service
|
||||
|
||||
# reload systemd service
|
||||
sudo systemctl daemon-reload
|
||||
|
||||
# start service
|
||||
sudo systemctl start nsite
|
||||
npx nsite-gateway
|
||||
```
|
||||
|
||||
Then once nsite-ts is running, next you need to configure nginx
|
||||
|
||||
Start by modifying the `/etx/nginx/nginx.conf` file and adding a `proxy_cache_path` to the `http` section
|
||||
## Running with docker-compose
|
||||
|
||||
```sh
|
||||
sudo nano /etc/nginx/nginx.conf
|
||||
git clone https://github.com/hzrd149/nsite-gateway.git
|
||||
cd nsite-gateway
|
||||
docker compose up
|
||||
```
|
||||
|
||||
```diff
|
||||
http {
|
||||
+ proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=request_cache:10m max_size=10g inactive=60m use_temp_path=off;
|
||||
}
|
||||
Once the service is running you can access the gateway at `http://localhost:3000`
|
||||
|
||||
## Running with docker
|
||||
|
||||
The `ghcr.io/hzrd149/nsite-gateway` image can be used to run a http instance locally
|
||||
|
||||
```sh
|
||||
docker run --rm -it --name nsite -p 3000:3000 ghcr.io/hzrd149/nsite-gateway
|
||||
```
|
||||
|
||||
Next modify the default site config (usually `/etx/nginx/sites-enabled/default` or `/etc/nginx/conf.d/default.conf`) to be one of
|
||||
- [nginx/http.conf](./nginx/http.conf)
|
||||
- [nginx/tls.conf](./nginx/tls.conf)
|
||||
- [nginx/tls-and-tor.conf](./nginx/tls-and-tor.conf)
|
||||
|
||||
Once that is done you can restart nginx and you should have a new nsite server running on port 80
|
||||
|
||||
## Tor setup
|
||||
|
||||
First you need to install tor (`sudo apt install tor` on debian systems) or [Documentation](https://community.torproject.org/onion-services/setup/install/)
|
||||
|
@ -4,7 +4,7 @@ After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
WorkingDirectory=/<path-to>/nsite-ts
|
||||
WorkingDirectory=/<path-to>/nsite-gateway
|
||||
ExecStart=/usr/bin/node .
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
|
@ -1,17 +1,36 @@
|
||||
version: "3.7"
|
||||
|
||||
services:
|
||||
redis:
|
||||
image: redis:alpine
|
||||
restart: unless-stopped
|
||||
command: redis-server --save 60 1 --loglevel warning
|
||||
volumes:
|
||||
- redis-data:/data
|
||||
|
||||
nsite:
|
||||
build: .
|
||||
image: ghcr.io/hzrd149/nsite-ts:master
|
||||
image: ghcr.io/hzrd149/nsite-gateway:master
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
LOOKUP_RELAYS: wss://user.kindpag.es,wss://purplepag.es
|
||||
SUBSCRIPTION_RELAYS: wss://nostrue.com/,wss://nos.lol/,wss://relay.damus.io/,wss://purplerelay.com/
|
||||
volumes:
|
||||
- type: tmpfs
|
||||
target: /var/cache/nginx
|
||||
tmpfs:
|
||||
size: 100M
|
||||
CACHE_PATH: redis://redis:6379
|
||||
depends_on:
|
||||
- redis
|
||||
|
||||
caddy:
|
||||
image: caddy:alpine
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- 8080:80
|
||||
- 3000:3000
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
volumes:
|
||||
- ./Caddyfile:/etc/caddy/Caddyfile:ro
|
||||
- caddy_data:/data
|
||||
- caddy_config:/config
|
||||
depends_on:
|
||||
- nsite
|
||||
|
||||
volumes:
|
||||
redis-data:
|
||||
caddy_data:
|
||||
caddy_config:
|
||||
|
@ -1,7 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
echo Changing permission on volumes
|
||||
chown -R nsite:nsite /var/cache/nginx
|
||||
chown -R nsite:nsite /screenshots
|
||||
|
||||
exec "$@"
|
@ -1,19 +0,0 @@
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name nsite;
|
||||
|
||||
location / {
|
||||
proxy_cache request_cache;
|
||||
proxy_cache_valid 200 60m;
|
||||
proxy_cache_valid 404 10m;
|
||||
proxy_cache_key $host$uri;
|
||||
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
|
||||
|
||||
expires 30d;
|
||||
add_header Cache-Control "public, no-transform";
|
||||
|
||||
proxy_set_header Host $host;
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
}
|
||||
}
|
@ -1,33 +0,0 @@
|
||||
user nsite;
|
||||
worker_processes auto;
|
||||
|
||||
error_log /dev/stderr notice;
|
||||
pid /var/run/nginx.pid;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
# add custom cache
|
||||
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=request_cache:10m max_size=10g inactive=60m use_temp_path=off;
|
||||
|
||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||
'$status $body_bytes_sent "$http_referer" '
|
||||
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||
|
||||
access_log /dev/stdout main;
|
||||
|
||||
sendfile on;
|
||||
#tcp_nopush on;
|
||||
|
||||
keepalive_timeout 65;
|
||||
|
||||
gzip on;
|
||||
|
||||
include /etc/nginx/conf.d/*.conf;
|
||||
}
|
||||
|
@ -1,56 +0,0 @@
|
||||
# tor .onion server
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name *.onion;
|
||||
|
||||
location / {
|
||||
proxy_cache request_cache;
|
||||
proxy_cache_valid 200 60m;
|
||||
proxy_cache_valid 404 10m;
|
||||
proxy_cache_key $host$uri;
|
||||
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
|
||||
|
||||
expires 30d;
|
||||
add_header Cache-Control "public, no-transform";
|
||||
|
||||
proxy_set_header Host $host;
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
}
|
||||
}
|
||||
|
||||
# redirect http to https
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name _;
|
||||
return 307 https://$host$request_uri;
|
||||
}
|
||||
|
||||
# http server
|
||||
server {
|
||||
listen 443 ssl;
|
||||
listen [::]:443 ssl;
|
||||
server_name nsite;
|
||||
|
||||
ssl_certificate /path/to/certificate/fullchain1.pem;
|
||||
ssl_certificate_key /path/to/certificate/privkey1.pem;
|
||||
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_prefer_server_ciphers on;
|
||||
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
|
||||
|
||||
location / {
|
||||
proxy_cache request_cache;
|
||||
proxy_cache_valid 200 60m;
|
||||
proxy_cache_valid 404 10m;
|
||||
proxy_cache_key $host$uri;
|
||||
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
|
||||
|
||||
expires 30d;
|
||||
add_header Cache-Control "public, no-transform";
|
||||
|
||||
proxy_set_header Host $host;
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
}
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
# redirect http to https
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name _;
|
||||
return 307 https://$host$request_uri;
|
||||
}
|
||||
|
||||
# nginx config for tls
|
||||
server {
|
||||
listen 443 ssl;
|
||||
listen [::]:443 ssl;
|
||||
server_name nsite;
|
||||
|
||||
ssl_certificate /path/to/certificate/fullchain1.pem;
|
||||
ssl_certificate_key /path/to/certificate/privkey1.pem;
|
||||
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_prefer_server_ciphers on;
|
||||
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
|
||||
|
||||
location / {
|
||||
proxy_cache request_cache;
|
||||
proxy_cache_valid 200 60m;
|
||||
proxy_cache_valid 404 10m;
|
||||
proxy_cache_key $host$uri;
|
||||
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
|
||||
|
||||
expires 30d;
|
||||
add_header Cache-Control "public, no-transform";
|
||||
|
||||
proxy_set_header Host $host;
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
}
|
||||
}
|
41
package.json
41
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "nsite-ts",
|
||||
"version": "0.6.0",
|
||||
"name": "nsite-gateway",
|
||||
"version": "1.0.1",
|
||||
"description": "A blossom server implementation written in Typescript",
|
||||
"main": "build/index.js",
|
||||
"type": "module",
|
||||
@ -19,33 +19,32 @@
|
||||
"public"
|
||||
],
|
||||
"dependencies": {
|
||||
"@keyv/redis": "^3.0.1",
|
||||
"@keyv/redis": "^4.3.2",
|
||||
"@keyv/sqlite": "^4.0.1",
|
||||
"@koa/cors": "^5.0.0",
|
||||
"blossom-client-sdk": "^2.1.1",
|
||||
"blossom-client-sdk": "^3.0.1",
|
||||
"debug": "^4.4.0",
|
||||
"dotenv": "^16.4.7",
|
||||
"follow-redirects": "^1.15.9",
|
||||
"keyv": "^5.2.3",
|
||||
"koa": "^2.15.3",
|
||||
"keyv": "^5.3.2",
|
||||
"koa": "^2.16.0",
|
||||
"koa-morgan": "^1.0.1",
|
||||
"koa-send": "^5.0.1",
|
||||
"koa-static": "^5.0.0",
|
||||
"mime": "^4.0.6",
|
||||
"nostr-tools": "^2.10.4",
|
||||
"nsite-cli": "^0.1.14",
|
||||
"pac-proxy-agent": "^7.1.0",
|
||||
"mime": "^4.0.7",
|
||||
"nostr-tools": "^2.12.0",
|
||||
"nsite-cli": "^0.1.16",
|
||||
"pac-proxy-agent": "^7.2.0",
|
||||
"proxy-agent": "^6.5.0",
|
||||
"puppeteer": "^23.11.1",
|
||||
"websocket-polyfill": "1.0.0",
|
||||
"ws": "^8.18.0",
|
||||
"ws": "^8.18.1",
|
||||
"xbytes": "^1.9.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@changesets/cli": "^2.27.11",
|
||||
"@swc-node/register": "^1.10.9",
|
||||
"@swc/core": "^1.10.9",
|
||||
"@types/better-sqlite3": "^7.6.12",
|
||||
"@changesets/cli": "^2.28.1",
|
||||
"@swc-node/register": "^1.10.10",
|
||||
"@swc/core": "^1.11.16",
|
||||
"@types/better-sqlite3": "^7.6.13",
|
||||
"@types/debug": "^4.1.12",
|
||||
"@types/follow-redirects": "^1.14.4",
|
||||
"@types/koa": "^2.15.0",
|
||||
@ -54,12 +53,14 @@
|
||||
"@types/koa-static": "^4.0.4",
|
||||
"@types/koa__cors": "^5.0.0",
|
||||
"@types/koa__router": "^12.0.4",
|
||||
"@types/node": "^20.17.14",
|
||||
"@types/node": "^20.17.30",
|
||||
"@types/proxy-from-env": "^1.0.4",
|
||||
"@types/ws": "^8.5.13",
|
||||
"@types/ws": "^8.18.1",
|
||||
"esbuild": "^0.25.2",
|
||||
"nodemon": "^3.1.9",
|
||||
"prettier": "^3.4.2",
|
||||
"typescript": "^5.7.3"
|
||||
"pkg": "^5.8.1",
|
||||
"prettier": "^3.5.3",
|
||||
"typescript": "^5.8.3"
|
||||
},
|
||||
"resolutions": {
|
||||
"websocket-polyfill": "1.0.0"
|
||||
|
2139
pnpm-lock.yaml
generated
2139
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
51
public/404.html
Normal file
51
public/404.html
Normal file
@ -0,0 +1,51 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>404 - Page Not Found</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
max-width: 800px;
|
||||
margin: 40px auto;
|
||||
padding: 0 20px;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
.container {
|
||||
background-color: white;
|
||||
padding: 30px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
h1 {
|
||||
color: #2c3e50;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.info {
|
||||
background-color: #f8f9fa;
|
||||
border-left: 4px solid #dc3545;
|
||||
padding: 15px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>404 - Page Not Found</h1>
|
||||
<div class="info">
|
||||
<p>We couldn't find an nsite for this domain.</p>
|
||||
<p>This could mean either:</p>
|
||||
<ul>
|
||||
<li>The domain is not configured to point to an nsite</li>
|
||||
</ul>
|
||||
</div>
|
||||
<p>
|
||||
For more information about setting up an nsite, please refer to the
|
||||
<a href="https://github.com/hzrd149/nsite-gateway">documentation</a>
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
After Width: 48px | Height: 48px | Size: 15 KiB |
@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Welcome to nsite-ts</title>
|
||||
<title>Welcome to nsite-gateway</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
@ -42,8 +42,8 @@
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Welcome to nsite-ts</h1>
|
||||
<p>If you're seeing this page, nsite-ts has been successfully installed and is working.</p>
|
||||
<h1>Welcome to nsite-gateway</h1>
|
||||
<p>If you're seeing this page, nsite-gateway has been successfully installed and is working.</p>
|
||||
|
||||
<div class="info">
|
||||
<p>
|
||||
@ -59,8 +59,8 @@
|
||||
</div>
|
||||
|
||||
<p>
|
||||
For more information about configuring nsite-ts, please refer to the
|
||||
<a href="https://github.com/hzrd149/nsite-ts">documentation</a>
|
||||
For more information about configuring nsite-gateway, please refer to the
|
||||
<a href="https://github.com/hzrd149/nsite-gateway">documentation</a>
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
|
@ -1,36 +1,67 @@
|
||||
import { getServersFromServerListEvent, USER_BLOSSOM_SERVER_LIST_KIND } from "blossom-client-sdk";
|
||||
import { IncomingMessage } from "node:http";
|
||||
|
||||
import { BLOSSOM_SERVERS, MAX_FILE_SIZE } from "./env.js";
|
||||
import { MAX_FILE_SIZE } from "./env.js";
|
||||
import { makeRequestWithAbort } from "./helpers/http.js";
|
||||
import pool from "./nostr.js";
|
||||
import { blobURLs } from "./cache.js";
|
||||
import logger from "./logger.js";
|
||||
|
||||
export async function getUserBlossomServers(pubkey: string, relays: string[]) {
|
||||
const blossomServersEvent = await pool.get(relays, { kinds: [USER_BLOSSOM_SERVER_LIST_KIND], authors: [pubkey] });
|
||||
const log = logger.extend("blossom");
|
||||
|
||||
return blossomServersEvent ? getServersFromServerListEvent(blossomServersEvent).map((u) => u.toString()) : undefined;
|
||||
/** Checks all servers for a blob and returns the URLs */
|
||||
export async function findBlobURLs(sha256: string, servers: string[]): Promise<string[]> {
|
||||
const cache = await blobURLs.get(sha256);
|
||||
if (cache) return cache;
|
||||
|
||||
const urls = await Promise.all(
|
||||
servers.map(async (server) => {
|
||||
const url = new URL(sha256, server);
|
||||
|
||||
const check = await fetch(url, { method: "HEAD" }).catch(() => null);
|
||||
if (check?.status === 200) return url.toString();
|
||||
else return null;
|
||||
}),
|
||||
);
|
||||
|
||||
const filtered = urls.filter((url) => url !== null);
|
||||
|
||||
log(`Found ${filtered.length}/${servers.length} URLs for ${sha256}`);
|
||||
await blobURLs.set(sha256, filtered);
|
||||
return filtered;
|
||||
}
|
||||
|
||||
// TODO: download the file to /tmp and verify it
|
||||
export async function downloadFile(sha256: string, servers = BLOSSOM_SERVERS) {
|
||||
for (const server of servers) {
|
||||
/** Downloads a file from multiple servers */
|
||||
export async function streamBlob(sha256: string, servers: string[]): Promise<IncomingMessage | undefined> {
|
||||
if (servers.length === 0) return undefined;
|
||||
|
||||
// First find all available URLs
|
||||
const urls = await findBlobURLs(sha256, servers);
|
||||
if (urls.length === 0) return undefined;
|
||||
|
||||
// Try each URL sequentially with timeout
|
||||
for (const urlString of urls) {
|
||||
const controller = new AbortController();
|
||||
let res: IncomingMessage | undefined = undefined;
|
||||
|
||||
try {
|
||||
const { response } = await makeRequestWithAbort(new URL(sha256, server));
|
||||
// Set up timeout to abort after 10s
|
||||
const timeout = setTimeout(() => {
|
||||
controller.abort();
|
||||
}, 10_000);
|
||||
|
||||
try {
|
||||
if (!response.statusCode) throw new Error("Missing headers or status code");
|
||||
const url = new URL(urlString);
|
||||
const response = await makeRequestWithAbort(url, controller);
|
||||
res = response;
|
||||
clearTimeout(timeout);
|
||||
|
||||
const size = response.headers["content-length"];
|
||||
if (size && parseInt(size) > MAX_FILE_SIZE) throw new Error("File too large");
|
||||
if (!response.statusCode) throw new Error("Missing headers or status code");
|
||||
|
||||
if (response.statusCode >= 200 && response.statusCode < 300) {
|
||||
return response;
|
||||
} else throw new Error("Request failed");
|
||||
} catch (error) {
|
||||
// Consume response data to free up memory
|
||||
response.resume();
|
||||
}
|
||||
const size = response.headers["content-length"];
|
||||
if (size && parseInt(size) > MAX_FILE_SIZE) throw new Error("File too large");
|
||||
|
||||
if (response.statusCode >= 200 && response.statusCode < 300) return response;
|
||||
} catch (error) {
|
||||
// ignore error, try next server
|
||||
if (res) res.resume();
|
||||
continue; // Try next URL if this one fails
|
||||
}
|
||||
}
|
||||
}
|
||||
|
58
src/cache.ts
58
src/cache.ts
@ -1,18 +1,19 @@
|
||||
import Keyv from "keyv";
|
||||
import pfs from "fs/promises";
|
||||
import { CACHE_PATH } from "./env.js";
|
||||
import Keyv, { KeyvOptions } from "keyv";
|
||||
import { CACHE_PATH, CACHE_TIME } from "./env.js";
|
||||
import logger from "./logger.js";
|
||||
import { ParsedEvent } from "./events.js";
|
||||
|
||||
try {
|
||||
await pfs.mkdir("data");
|
||||
} catch (error) {}
|
||||
const log = logger.extend("cache");
|
||||
|
||||
async function createStore() {
|
||||
if (!CACHE_PATH || CACHE_PATH === "in-memory") return undefined;
|
||||
else if (CACHE_PATH.startsWith("redis://")) {
|
||||
const { default: KeyvRedis } = await import("@keyv/redis");
|
||||
log(`Using redis cache at ${CACHE_PATH}`);
|
||||
return new KeyvRedis(CACHE_PATH);
|
||||
} else if (CACHE_PATH.startsWith("sqlite://")) {
|
||||
const { default: KeyvSqlite } = await import("@keyv/sqlite");
|
||||
log(`Using sqlite cache at ${CACHE_PATH}`);
|
||||
return new KeyvSqlite(CACHE_PATH);
|
||||
}
|
||||
}
|
||||
@ -20,32 +21,49 @@ async function createStore() {
|
||||
const store = await createStore();
|
||||
|
||||
store?.on("error", (err) => {
|
||||
console.log("Connection Error", err);
|
||||
log("Connection Error", err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
const opts = store ? { store } : {};
|
||||
const json: KeyvOptions = { serialize: JSON.stringify, deserialize: JSON.parse };
|
||||
const opts: KeyvOptions = store ? { store } : {};
|
||||
|
||||
/** domain -> pubkey */
|
||||
export const userDomains = new Keyv({
|
||||
/** A cache that maps a domain to a pubkey ( domain -> pubkey ) */
|
||||
export const pubkeyDomains = new Keyv<string | undefined>({
|
||||
...opts,
|
||||
...json,
|
||||
namespace: "domains",
|
||||
// cache domains for an hour
|
||||
ttl: 60 * 60 * 1000,
|
||||
ttl: CACHE_TIME * 1000,
|
||||
});
|
||||
|
||||
/** pubkey -> blossom servers */
|
||||
export const userServers = new Keyv({
|
||||
/** A cache that maps a pubkey to a set of blossom servers ( pubkey -> servers ) */
|
||||
export const pubkeyServers = new Keyv<string[] | undefined>({
|
||||
...opts,
|
||||
...json,
|
||||
namespace: "servers",
|
||||
// cache servers for an hour
|
||||
ttl: 60 * 60 * 1000,
|
||||
ttl: CACHE_TIME * 1000,
|
||||
});
|
||||
|
||||
/** pubkey -> relays */
|
||||
export const userRelays = new Keyv({
|
||||
/** A cache that maps a pubkey to a set of relays ( pubkey -> relays ) */
|
||||
export const pubkeyRelays = new Keyv<string[] | undefined>({
|
||||
...opts,
|
||||
...json,
|
||||
namespace: "relays",
|
||||
// cache relays for an hour
|
||||
ttl: 60 * 60 * 1000,
|
||||
ttl: CACHE_TIME * 1000,
|
||||
});
|
||||
|
||||
/** A cache that maps a pubkey + path to sha256 hash of the blob ( pubkey/path -> sha256 ) */
|
||||
export const pathBlobs = new Keyv<ParsedEvent | undefined>({
|
||||
...opts,
|
||||
...json,
|
||||
namespace: "paths",
|
||||
ttl: CACHE_TIME * 1000,
|
||||
});
|
||||
|
||||
/** A cache that maps a sha256 hash to a set of URLs that had the blob ( sha256 -> URLs ) */
|
||||
export const blobURLs = new Keyv<string[] | undefined>({
|
||||
...opts,
|
||||
...json,
|
||||
namespace: "blobs",
|
||||
ttl: CACHE_TIME * 1000,
|
||||
});
|
||||
|
95
src/dns.ts
Normal file
95
src/dns.ts
Normal file
@ -0,0 +1,95 @@
|
||||
import dns from "node:dns";
|
||||
import { nip05, nip19 } from "nostr-tools";
|
||||
import { pubkeyDomains as pubkeyDomains } from "./cache.js";
|
||||
import logger from "./logger.js";
|
||||
import { NIP05_NAME_DOMAINS } from "./env.js";
|
||||
|
||||
export function getCnameRecords(hostname: string): Promise<string[]> {
|
||||
return new Promise<string[]>((res, rej) => {
|
||||
dns.resolveCname(hostname, (err, records) => {
|
||||
if (err) rej(err);
|
||||
else res(records);
|
||||
});
|
||||
});
|
||||
}
|
||||
export function getTxtRecords(hostname: string): Promise<string[][]> {
|
||||
return new Promise<string[][]>((res, rej) => {
|
||||
dns.resolveTxt(hostname, (err, records) => {
|
||||
if (err) rej(err);
|
||||
else res(records);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function extractPubkeyFromHostname(hostname: string): string | undefined {
|
||||
const [npub] = hostname.split(".");
|
||||
|
||||
if (npub.startsWith("npub")) {
|
||||
const parsed = nip19.decode(npub);
|
||||
if (parsed.type !== "npub") throw new Error("Expected npub");
|
||||
|
||||
return parsed.data;
|
||||
}
|
||||
}
|
||||
|
||||
const log = logger.extend("DNS");
|
||||
|
||||
export async function resolvePubkeyFromHostname(hostname: string): Promise<string | undefined> {
|
||||
if (hostname === "localhost") return undefined;
|
||||
|
||||
const cached = await pubkeyDomains.get(hostname);
|
||||
if (cached) return cached;
|
||||
|
||||
// check if domain contains an npub
|
||||
let pubkey = extractPubkeyFromHostname(hostname);
|
||||
|
||||
if (!pubkey) {
|
||||
// try to get npub from CNAME
|
||||
try {
|
||||
const cnameRecords = await getCnameRecords(hostname);
|
||||
for (const cname of cnameRecords) {
|
||||
const p = extractPubkeyFromHostname(cname);
|
||||
if (p) {
|
||||
pubkey = p;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (error) {}
|
||||
}
|
||||
|
||||
if (!pubkey) {
|
||||
// Try to get npub from TXT records
|
||||
try {
|
||||
const txtRecords = await getTxtRecords(hostname);
|
||||
|
||||
for (const txt of txtRecords) {
|
||||
for (const entry of txt) {
|
||||
const p = extractPubkeyFromHostname(entry);
|
||||
if (p) {
|
||||
pubkey = p;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {}
|
||||
}
|
||||
|
||||
// Try to get npub from NIP-05
|
||||
if (!pubkey && NIP05_NAME_DOMAINS) {
|
||||
for (const domain of NIP05_NAME_DOMAINS) {
|
||||
try {
|
||||
const [name] = hostname.split(".");
|
||||
const result = await nip05.queryProfile(name + "@" + domain);
|
||||
if (result) {
|
||||
pubkey = result.pubkey;
|
||||
break;
|
||||
}
|
||||
} catch (err) {}
|
||||
}
|
||||
}
|
||||
|
||||
log(`Resolved ${hostname} to ${pubkey}`);
|
||||
await pubkeyDomains.set(hostname, pubkey);
|
||||
|
||||
return pubkey;
|
||||
}
|
14
src/env.ts
14
src/env.ts
@ -16,8 +16,11 @@ const BLOSSOM_SERVERS = process.env.BLOSSOM_SERVERS?.split(",").map((u) => u.tri
|
||||
|
||||
const MAX_FILE_SIZE = process.env.MAX_FILE_SIZE ? xbytes.parseSize(process.env.MAX_FILE_SIZE) : Infinity;
|
||||
|
||||
const NGINX_CACHE_DIR = process.env.NGINX_CACHE_DIR;
|
||||
const CACHE_PATH = process.env.CACHE_PATH;
|
||||
const CACHE_TIME = process.env.CACHE_TIME ? parseInt(process.env.CACHE_TIME) : 60 * 60;
|
||||
|
||||
const NIP05_NAME_DOMAINS = process.env.NIP05_NAME_DOMAINS?.split(",").map((d) => d.trim());
|
||||
const PUBLIC_DOMAIN = process.env.PUBLIC_DOMAIN;
|
||||
|
||||
const PAC_PROXY = process.env.PAC_PROXY;
|
||||
const TOR_PROXY = process.env.TOR_PROXY;
|
||||
@ -27,9 +30,6 @@ const NSITE_HOST = process.env.NSITE_HOST || "0.0.0.0";
|
||||
const NSITE_PORT = process.env.NSITE_PORT ? parseInt(process.env.NSITE_PORT) : 3000;
|
||||
const HOST = `${NSITE_HOST}:${NSITE_PORT}`;
|
||||
|
||||
const ENABLE_SCREENSHOTS = process.env.ENABLE_SCREENSHOTS === "true";
|
||||
const SCREENSHOTS_DIR = process.env.SCREENSHOTS_DIR || "./screenshots";
|
||||
|
||||
const ONION_HOST = process.env.ONION_HOST;
|
||||
|
||||
export {
|
||||
@ -39,7 +39,6 @@ export {
|
||||
LOOKUP_RELAYS,
|
||||
BLOSSOM_SERVERS,
|
||||
MAX_FILE_SIZE,
|
||||
NGINX_CACHE_DIR,
|
||||
CACHE_PATH,
|
||||
PAC_PROXY,
|
||||
TOR_PROXY,
|
||||
@ -47,7 +46,8 @@ export {
|
||||
NSITE_HOST,
|
||||
NSITE_PORT,
|
||||
HOST,
|
||||
ENABLE_SCREENSHOTS,
|
||||
SCREENSHOTS_DIR,
|
||||
ONION_HOST,
|
||||
CACHE_TIME,
|
||||
NIP05_NAME_DOMAINS,
|
||||
PUBLIC_DOMAIN,
|
||||
};
|
||||
|
@ -1,7 +1,16 @@
|
||||
import { extname, isAbsolute, join } from "path";
|
||||
import { extname, join } from "path";
|
||||
import { NSITE_KIND } from "./const.js";
|
||||
import { requestEvents } from "./nostr.js";
|
||||
import { pathBlobs } from "./cache.js";
|
||||
|
||||
export type ParsedEvent = {
|
||||
pubkey: string;
|
||||
path: string;
|
||||
sha256: string;
|
||||
created_at: number;
|
||||
};
|
||||
|
||||
/** Returns all the `d` tags that should be searched for a given path */
|
||||
export function getSearchPaths(path: string) {
|
||||
const paths = [path];
|
||||
|
||||
@ -11,7 +20,7 @@ export function getSearchPaths(path: string) {
|
||||
return paths.filter((p) => !!p);
|
||||
}
|
||||
|
||||
export function parseNsiteEvent(event: { pubkey: string; tags: string[][] }) {
|
||||
export function parseNsiteEvent(event: { pubkey: string; tags: string[][]; created_at: number }) {
|
||||
const path = event.tags.find((t) => t[0] === "d" && t[1])?.[1];
|
||||
const sha256 = event.tags.find((t) => t[0] === "x" && t[1])?.[1];
|
||||
|
||||
@ -20,16 +29,29 @@ export function parseNsiteEvent(event: { pubkey: string; tags: string[][] }) {
|
||||
pubkey: event.pubkey,
|
||||
path: join("/", path),
|
||||
sha256,
|
||||
created_at: event.created_at,
|
||||
};
|
||||
}
|
||||
|
||||
export async function getNsiteBlobs(pubkey: string, path: string, relays: string[]) {
|
||||
/** Returns the first blob found for a given path */
|
||||
export async function getNsiteBlob(pubkey: string, path: string, relays: string[]): Promise<ParsedEvent | undefined> {
|
||||
const key = pubkey + path;
|
||||
|
||||
const cached = await pathBlobs.get(key);
|
||||
if (cached) return cached;
|
||||
|
||||
// NOTE: hack, remove "/" paths since it breaks some relays
|
||||
const paths = getSearchPaths(path).filter((p) => p !== "/");
|
||||
const events = await requestEvents(relays, { kinds: [NSITE_KIND], "#d": paths, authors: [pubkey] });
|
||||
|
||||
return Array.from(events)
|
||||
// Sort the found blobs by the order of the paths array
|
||||
const options = Array.from(events)
|
||||
.map(parseNsiteEvent)
|
||||
.filter((e) => !!e)
|
||||
.sort((a, b) => paths.indexOf(a.path) - paths.indexOf(b.path));
|
||||
|
||||
// Remember the blob for this path
|
||||
if (options.length > 0) await pathBlobs.set(key, options[0]);
|
||||
|
||||
return options[0];
|
||||
}
|
||||
|
@ -1,59 +0,0 @@
|
||||
import dns from "node:dns";
|
||||
import { nip19 } from "nostr-tools";
|
||||
|
||||
export function getCnameRecords(hostname: string) {
|
||||
return new Promise<string[]>((res, rej) => {
|
||||
dns.resolveCname(hostname, (err, records) => {
|
||||
if (err) rej(err);
|
||||
else res(records);
|
||||
});
|
||||
});
|
||||
}
|
||||
export function getTxtRecords(hostname: string) {
|
||||
return new Promise<string[][]>((res, rej) => {
|
||||
dns.resolveTxt(hostname, (err, records) => {
|
||||
if (err) rej(err);
|
||||
else res(records);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function extractNpubFromHostname(hostname: string) {
|
||||
const [npub] = hostname.split(".");
|
||||
|
||||
if (npub.startsWith("npub")) {
|
||||
const parsed = nip19.decode(npub);
|
||||
if (parsed.type !== "npub") throw new Error("Expected npub");
|
||||
|
||||
return parsed.data;
|
||||
}
|
||||
}
|
||||
|
||||
export async function resolveNpubFromHostname(hostname: string) {
|
||||
// check if domain contains an npub
|
||||
let pubkey = extractNpubFromHostname(hostname);
|
||||
|
||||
if (pubkey) return pubkey;
|
||||
|
||||
if (hostname === "localhost") return undefined;
|
||||
|
||||
// try to get npub from CNAME or TXT records
|
||||
try {
|
||||
const cnameRecords = await getCnameRecords(hostname);
|
||||
for (const cname of cnameRecords) {
|
||||
const p = extractNpubFromHostname(cname);
|
||||
if (p) return p;
|
||||
}
|
||||
} catch (error) {}
|
||||
|
||||
try {
|
||||
const txtRecords = await getTxtRecords(hostname);
|
||||
|
||||
for (const txt of txtRecords) {
|
||||
for (const entry of txt) {
|
||||
const p = extractNpubFromHostname(entry);
|
||||
if (p) return p;
|
||||
}
|
||||
}
|
||||
} catch (error) {}
|
||||
}
|
@ -4,17 +4,18 @@ const { http, https } = followRedirects;
|
||||
|
||||
import agent from "../proxy.js";
|
||||
|
||||
export function makeRequestWithAbort(url: URL) {
|
||||
return new Promise<{ response: IncomingMessage; controller: AbortController }>((res, rej) => {
|
||||
const cancelController = new AbortController();
|
||||
export function makeRequestWithAbort(url: URL, controller: AbortController) {
|
||||
return new Promise<IncomingMessage>((res, rej) => {
|
||||
controller.signal.addEventListener("abort", () => rej(new Error("Aborted")));
|
||||
|
||||
const request = (url.protocol === "https:" ? https : http).get(
|
||||
url,
|
||||
{
|
||||
signal: cancelController.signal,
|
||||
signal: controller.signal,
|
||||
agent,
|
||||
},
|
||||
(response) => {
|
||||
res({ response, controller: cancelController });
|
||||
res(response);
|
||||
},
|
||||
);
|
||||
request.on("error", (err) => rej(err));
|
||||
|
246
src/index.ts
246
src/index.ts
@ -2,36 +2,33 @@
|
||||
import "./polyfill.js";
|
||||
import Koa from "koa";
|
||||
import serve from "koa-static";
|
||||
import path, { basename } from "node:path";
|
||||
import path from "node:path";
|
||||
import cors from "@koa/cors";
|
||||
import fs from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import mime from "mime";
|
||||
import morgan from "koa-morgan";
|
||||
import send from "koa-send";
|
||||
import { npubEncode } from "nostr-tools/nip19";
|
||||
import { spawn } from "node:child_process";
|
||||
import { nip19 } from "nostr-tools";
|
||||
|
||||
import { resolveNpubFromHostname } from "./helpers/dns.js";
|
||||
import { getNsiteBlobs, parseNsiteEvent } from "./events.js";
|
||||
import { downloadFile, getUserBlossomServers } from "./blossom.js";
|
||||
import { resolvePubkeyFromHostname } from "./dns.js";
|
||||
import { getNsiteBlob } from "./events.js";
|
||||
import { streamBlob } from "./blossom.js";
|
||||
import {
|
||||
BLOSSOM_SERVERS,
|
||||
ENABLE_SCREENSHOTS,
|
||||
HOST,
|
||||
NGINX_CACHE_DIR,
|
||||
NSITE_HOMEPAGE,
|
||||
NSITE_HOMEPAGE_DIR,
|
||||
NSITE_HOST,
|
||||
NSITE_PORT,
|
||||
ONION_HOST,
|
||||
PUBLIC_DOMAIN,
|
||||
SUBSCRIPTION_RELAYS,
|
||||
} from "./env.js";
|
||||
import { userDomains, userRelays, userServers } from "./cache.js";
|
||||
import { invalidatePubkeyPath } from "./nginx.js";
|
||||
import pool, { getUserOutboxes, subscribeForEvents } from "./nostr.js";
|
||||
import pool, { getUserBlossomServers, getUserOutboxes } from "./nostr.js";
|
||||
import logger from "./logger.js";
|
||||
import { watchInvalidation } from "./invalidation.js";
|
||||
import { NSITE_KIND } from "./const.js";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
@ -64,111 +61,93 @@ app.use(async (ctx, next) => {
|
||||
|
||||
// handle nsite requests
|
||||
app.use(async (ctx, next) => {
|
||||
let pubkey = await userDomains.get<string | undefined>(ctx.hostname);
|
||||
let pubkey = await resolvePubkeyFromHostname(ctx.hostname);
|
||||
|
||||
// resolve pubkey if not in cache
|
||||
if (pubkey === undefined) {
|
||||
logger(`${ctx.hostname}: Resolving`);
|
||||
pubkey = await resolveNpubFromHostname(ctx.hostname);
|
||||
let fallthrough = false;
|
||||
if (!pubkey && NSITE_HOMEPAGE && (!PUBLIC_DOMAIN || ctx.hostname === PUBLIC_DOMAIN)) {
|
||||
const parsed = nip19.decode(NSITE_HOMEPAGE);
|
||||
// TODO: use the relays in the nprofile
|
||||
|
||||
if (pubkey) {
|
||||
await userDomains.set(ctx.hostname, pubkey);
|
||||
logger(`${ctx.hostname}: Found ${pubkey}`);
|
||||
} else {
|
||||
await userDomains.set(ctx.hostname, "");
|
||||
}
|
||||
if (parsed.type === "nprofile") pubkey = parsed.data.pubkey;
|
||||
else if (parsed.type === "npub") pubkey = parsed.data;
|
||||
|
||||
// Fallback to public dir if path cannot be found on the nsite homepage
|
||||
if (pubkey) fallthrough = true;
|
||||
}
|
||||
|
||||
if (pubkey) {
|
||||
const npub = npubEncode(pubkey);
|
||||
const log = logger.extend(npub);
|
||||
ctx.state.pubkey = pubkey;
|
||||
if (!pubkey) {
|
||||
if (fallthrough) return next();
|
||||
|
||||
let relays = await userRelays.get<string[] | undefined>(pubkey);
|
||||
ctx.status = 404;
|
||||
ctx.body = fs.readFileSync(path.resolve(__dirname, "../public/404.html"), "utf-8");
|
||||
return;
|
||||
}
|
||||
|
||||
// fetch relays if not in cache
|
||||
if (!relays) {
|
||||
log(`Fetching relays`);
|
||||
// fetch relays
|
||||
const relays = (await getUserOutboxes(pubkey)) || [];
|
||||
|
||||
relays = await getUserOutboxes(pubkey);
|
||||
if (relays) {
|
||||
await userRelays.set(pubkey, relays);
|
||||
log(`Found ${relays.length} relays`);
|
||||
} else {
|
||||
relays = [];
|
||||
await userServers.set(pubkey, [], 30_000);
|
||||
log(`Failed to find relays`);
|
||||
}
|
||||
}
|
||||
// always check subscription relays
|
||||
relays.push(...SUBSCRIPTION_RELAYS);
|
||||
|
||||
relays.push(...SUBSCRIPTION_RELAYS);
|
||||
if (relays.length === 0) throw new Error("No relays found");
|
||||
|
||||
if (relays.length === 0) throw new Error("No nostr relays");
|
||||
// fetch servers and events in parallel
|
||||
let [servers, event] = await Promise.all([
|
||||
getUserBlossomServers(pubkey, relays).then((s) => s || []),
|
||||
getNsiteBlob(pubkey, ctx.path, relays).then((e) => {
|
||||
if (!e) return getNsiteBlob(pubkey, "/404.html", relays);
|
||||
else return e;
|
||||
}),
|
||||
]);
|
||||
|
||||
log(`Searching for ${ctx.path}`);
|
||||
let blobs = await getNsiteBlobs(pubkey, ctx.path, relays);
|
||||
if (!event) {
|
||||
if (fallthrough) return next();
|
||||
|
||||
if (blobs.length === 0) {
|
||||
// fallback to custom 404 page
|
||||
log(`Looking for custom 404 page`);
|
||||
blobs = await getNsiteBlobs(pubkey, "/404.html", relays);
|
||||
}
|
||||
ctx.status = 404;
|
||||
ctx.body = `Not Found: no events found\npath: ${ctx.path}\nkind: ${NSITE_KIND}\npubkey: ${pubkey}\nrelays: ${relays.join(", ")}`;
|
||||
return;
|
||||
}
|
||||
|
||||
if (blobs.length === 0) {
|
||||
log(`Found 0 events`);
|
||||
ctx.status = 404;
|
||||
ctx.body = "Not Found";
|
||||
// always fetch from additional servers
|
||||
servers.push(...BLOSSOM_SERVERS);
|
||||
|
||||
if (servers.length === 0) throw new Error("Failed to find blossom servers");
|
||||
|
||||
try {
|
||||
const res = await streamBlob(event.sha256, servers);
|
||||
if (!res) {
|
||||
ctx.status = 502;
|
||||
ctx.body = `Failed to find blob\npath: ${event.path}\nsha256: ${event.sha256}\nservers: ${servers.join(", ")}`;
|
||||
return;
|
||||
}
|
||||
|
||||
let servers = await userServers.get<string[] | undefined>(pubkey);
|
||||
const type = mime.getType(event.path);
|
||||
if (type) ctx.set("content-type", type);
|
||||
else if (res.headers["content-type"]) ctx.set("content-type", res.headers["content-type"]);
|
||||
|
||||
// fetch blossom servers if not in cache
|
||||
if (!servers) {
|
||||
log(`Fetching blossom servers`);
|
||||
servers = await getUserBlossomServers(pubkey, relays);
|
||||
// pass headers along
|
||||
if (res.headers["content-length"]) ctx.set("content-length", res.headers["content-length"]);
|
||||
|
||||
if (servers) {
|
||||
await userServers.set(pubkey, servers);
|
||||
log(`Found ${servers.length} servers`);
|
||||
} else {
|
||||
servers = [];
|
||||
await userServers.set(pubkey, [], 30_000);
|
||||
log(`Failed to find servers`);
|
||||
}
|
||||
// set Onion-Location header
|
||||
if (ONION_HOST) {
|
||||
const url = new URL(ONION_HOST);
|
||||
url.hostname = npubEncode(pubkey) + "." + url.hostname;
|
||||
ctx.set("Onion-Location", url.toString().replace(/\/$/, ""));
|
||||
}
|
||||
|
||||
// always fetch from additional servers
|
||||
servers.push(...BLOSSOM_SERVERS);
|
||||
|
||||
for (const blob of blobs) {
|
||||
const res = await downloadFile(blob.sha256, servers);
|
||||
|
||||
if (res) {
|
||||
const type = mime.getType(blob.path);
|
||||
if (type) ctx.set("content-type", type);
|
||||
else if (res.headers["content-type"]) ctx.set("content-type", res.headers["content-type"]);
|
||||
|
||||
// pass headers along
|
||||
if (res.headers["content-length"]) ctx.set("content-length", res.headers["content-length"]);
|
||||
if (res.headers["last-modified"]) ctx.set("last-modified", res.headers["last-modified"]);
|
||||
|
||||
// set Onion-Location header
|
||||
if (ONION_HOST) {
|
||||
const url = new URL(ONION_HOST);
|
||||
url.hostname = npubEncode(pubkey) + "." + url.hostname;
|
||||
ctx.set("Onion-Location", url.toString().replace(/\/$/, ""));
|
||||
}
|
||||
|
||||
ctx.status = 200;
|
||||
ctx.body = res;
|
||||
return;
|
||||
}
|
||||
}
|
||||
// add cache headers
|
||||
ctx.set("ETag", res.headers["etag"] || `"${event.sha256}"`);
|
||||
ctx.set("Cache-Control", "public, max-age=3600");
|
||||
ctx.set("Last-Modified", res.headers["last-modified"] || new Date(event.created_at * 1000).toUTCString());
|
||||
|
||||
ctx.status = 200;
|
||||
ctx.body = res;
|
||||
return;
|
||||
} catch (error) {
|
||||
ctx.status = 500;
|
||||
ctx.body = "Failed to find blob";
|
||||
} else await next();
|
||||
ctx.body = `Failed to stream blob ${event.path}\n${error}`;
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
if (ONION_HOST) {
|
||||
@ -182,55 +161,6 @@ if (ONION_HOST) {
|
||||
});
|
||||
}
|
||||
|
||||
// get screenshots for websites
|
||||
if (ENABLE_SCREENSHOTS) {
|
||||
app.use(async (ctx, next) => {
|
||||
if (ctx.method === "GET" && ctx.path.startsWith("/screenshot")) {
|
||||
const [pubkey, etx] = basename(ctx.path).split(".");
|
||||
|
||||
if (pubkey) {
|
||||
const { hasScreenshot, takeScreenshot, getScreenshotPath } = await import("./screenshots.js");
|
||||
if (!(await hasScreenshot(pubkey))) await takeScreenshot(pubkey);
|
||||
|
||||
await send(ctx, getScreenshotPath(pubkey));
|
||||
} else throw Error("Missing pubkey");
|
||||
} else return next();
|
||||
});
|
||||
}
|
||||
|
||||
// download homepage
|
||||
if (NSITE_HOMEPAGE) {
|
||||
try {
|
||||
const log = logger.extend("homepage");
|
||||
// create the public dir
|
||||
try {
|
||||
fs.mkdirSync(NSITE_HOMEPAGE_DIR);
|
||||
} catch (error) {}
|
||||
|
||||
const bin = (await import.meta.resolve("nsite-cli")).replace("file://", "");
|
||||
|
||||
const decode = nip19.decode(NSITE_HOMEPAGE);
|
||||
if (decode.type !== "nprofile") throw new Error("NSITE_HOMEPAGE must be a valid nprofile");
|
||||
|
||||
// use nsite-cli to download the homepage
|
||||
const args = [bin, "download", NSITE_HOMEPAGE_DIR, nip19.npubEncode(decode.data.pubkey)];
|
||||
if (decode.data.relays) args.push("--relays", decode.data.relays?.join(","));
|
||||
|
||||
const child = spawn("node", args, { stdio: "pipe" });
|
||||
|
||||
child.on("spawn", () => log("Downloading..."));
|
||||
child.stdout.on("data", (line) => log(line.toString("utf-8")));
|
||||
child.on("error", (e) => log("Failed", e));
|
||||
child.on("close", (code) => {
|
||||
if (code === 0) log("Finished");
|
||||
else log("Failed");
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(`Failed to download homepage`);
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
// serve static files from public
|
||||
const serveOptions: serve.Options = {
|
||||
hidden: true,
|
||||
@ -247,35 +177,13 @@ try {
|
||||
app.use(serve(www, serveOptions));
|
||||
}
|
||||
|
||||
// start the server
|
||||
app.listen({ host: NSITE_HOST, port: NSITE_PORT }, () => {
|
||||
logger("Started on port", HOST);
|
||||
});
|
||||
|
||||
// invalidate nginx cache and screenshots on new events
|
||||
if (SUBSCRIPTION_RELAYS.length > 0) {
|
||||
logger(`Listening for new nsite events on: ${SUBSCRIPTION_RELAYS.join(", ")}`);
|
||||
|
||||
subscribeForEvents(SUBSCRIPTION_RELAYS, async (event) => {
|
||||
try {
|
||||
const nsite = parseNsiteEvent(event);
|
||||
if (nsite) {
|
||||
const log = logger.extend(nip19.npubEncode(nsite.pubkey));
|
||||
if (NGINX_CACHE_DIR) {
|
||||
log(`Invalidating ${nsite.path}`);
|
||||
await invalidatePubkeyPath(nsite.pubkey, nsite.path);
|
||||
}
|
||||
|
||||
// invalidate screenshot for nsite
|
||||
if (ENABLE_SCREENSHOTS && (nsite.path === "/" || nsite.path === "/index.html")) {
|
||||
const { removeScreenshot } = await import("./screenshots.js");
|
||||
await removeScreenshot(nsite.pubkey);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(`Failed to invalidate ${event.id}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
// watch for invalidations
|
||||
watchInvalidation();
|
||||
|
||||
process.on("unhandledRejection", (reason, promise) => {
|
||||
console.error("Unhandled Rejection at:", promise, "reason:", reason);
|
||||
|
31
src/invalidation.ts
Normal file
31
src/invalidation.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { npubEncode } from "nostr-tools/nip19";
|
||||
|
||||
import { SUBSCRIPTION_RELAYS } from "./env.js";
|
||||
import { parseNsiteEvent } from "./events.js";
|
||||
import pool from "./nostr.js";
|
||||
import { NSITE_KIND } from "./const.js";
|
||||
import logger from "./logger.js";
|
||||
import { pathBlobs } from "./cache.js";
|
||||
|
||||
const log = logger.extend("invalidation");
|
||||
|
||||
export function watchInvalidation() {
|
||||
if (SUBSCRIPTION_RELAYS.length === 0) return;
|
||||
|
||||
logger(`Listening for new nsite events on: ${SUBSCRIPTION_RELAYS.join(", ")}`);
|
||||
|
||||
pool.subscribeMany(SUBSCRIPTION_RELAYS, [{ kinds: [NSITE_KIND], since: Math.round(Date.now() / 1000) - 60 * 60 }], {
|
||||
onevent: async (event) => {
|
||||
try {
|
||||
const parsed = parseNsiteEvent(event);
|
||||
if (parsed) {
|
||||
pathBlobs.delete(parsed.pubkey + parsed.path);
|
||||
|
||||
log(`Invalidated ${npubEncode(parsed.pubkey) + parsed.path}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(`Failed to invalidate ${event.id}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
37
src/nginx.ts
37
src/nginx.ts
@ -1,37 +0,0 @@
|
||||
import pfs from "node:fs/promises";
|
||||
import crypto from "node:crypto";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { NGINX_CACHE_DIR } from "./env.js";
|
||||
import { userDomains } from "./cache.js";
|
||||
|
||||
export async function invalidatePubkeyPath(pubkey: string, path: string) {
|
||||
const iterator = userDomains.iterator?.(undefined);
|
||||
if (!iterator) return;
|
||||
|
||||
const promises: Promise<boolean | undefined>[] = [];
|
||||
for await (const [domain, key] of iterator) {
|
||||
if (key === pubkey) {
|
||||
promises.push(invalidateNginxCache(domain, path));
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.allSettled(promises);
|
||||
}
|
||||
|
||||
export async function invalidateNginxCache(host: string, path: string) {
|
||||
if (!NGINX_CACHE_DIR) return Promise.resolve(false);
|
||||
|
||||
try {
|
||||
const key = `${host}${path}`;
|
||||
const md5 = crypto.createHash("md5").update(key).digest("hex");
|
||||
|
||||
// NOTE: hard coded to cache levels 1:2
|
||||
const cachePath = join(NGINX_CACHE_DIR, md5.slice(-1), md5.slice(-3, -1), md5);
|
||||
await pfs.rm(cachePath);
|
||||
|
||||
console.log(`Invalidated ${key} (${md5})`);
|
||||
} catch (error) {
|
||||
// ignore errors
|
||||
}
|
||||
}
|
43
src/nostr.ts
43
src/nostr.ts
@ -1,20 +1,51 @@
|
||||
import { Filter, NostrEvent, SimplePool } from "nostr-tools";
|
||||
import { getServersFromServerListEvent, USER_BLOSSOM_SERVER_LIST_KIND } from "blossom-client-sdk";
|
||||
|
||||
import { LOOKUP_RELAYS } from "./env.js";
|
||||
import { NSITE_KIND } from "./const.js";
|
||||
import { pubkeyRelays, pubkeyServers } from "./cache.js";
|
||||
import logger from "./logger.js";
|
||||
import { npubEncode } from "nostr-tools/nip19";
|
||||
|
||||
const pool = new SimplePool();
|
||||
|
||||
const log = logger.extend("nostr");
|
||||
|
||||
/** Fetches a pubkeys mailboxes from the cache or relays */
|
||||
export async function getUserOutboxes(pubkey: string) {
|
||||
const cached = await pubkeyRelays.get(pubkey);
|
||||
if (cached) return cached;
|
||||
const mailboxes = await pool.get(LOOKUP_RELAYS, { kinds: [10002], authors: [pubkey] });
|
||||
|
||||
if (!mailboxes) return;
|
||||
|
||||
return mailboxes.tags.filter((t) => t[0] === "r" && (t[2] === undefined || t[2] === "write")).map((t) => t[1]);
|
||||
const relays = mailboxes.tags
|
||||
.filter((t) => t[0] === "r" && (t[2] === undefined || t[2] === "write"))
|
||||
.map((t) => t[1]);
|
||||
|
||||
log(`Found ${relays.length} relays for ${npubEncode(pubkey)}`);
|
||||
await pubkeyRelays.set(pubkey, relays);
|
||||
|
||||
await pubkeyRelays.set(pubkey, relays);
|
||||
return relays;
|
||||
}
|
||||
|
||||
export function subscribeForEvents(relays: string[], onevent: (event: NostrEvent) => any) {
|
||||
return pool.subscribeMany(relays, [{ kinds: [NSITE_KIND], since: Math.round(Date.now() / 1000) - 60 * 60 }], {
|
||||
onevent,
|
||||
});
|
||||
/** Fetches a pubkeys blossom servers from the cache or relays */
|
||||
export async function getUserBlossomServers(pubkey: string, relays: string[]) {
|
||||
const cached = await pubkeyServers.get(pubkey);
|
||||
if (cached) return cached;
|
||||
|
||||
const blossomServersEvent = await pool.get(relays, { kinds: [USER_BLOSSOM_SERVER_LIST_KIND], authors: [pubkey] });
|
||||
const servers = blossomServersEvent
|
||||
? getServersFromServerListEvent(blossomServersEvent).map((u) => u.toString())
|
||||
: undefined;
|
||||
|
||||
// Save servers if found
|
||||
if (servers) {
|
||||
log(`Found ${servers.length} blossom servers for ${npubEncode(pubkey)}`);
|
||||
await pubkeyServers.set(pubkey, servers);
|
||||
}
|
||||
|
||||
return servers;
|
||||
}
|
||||
|
||||
export function requestEvents(relays: string[], filter: Filter) {
|
||||
|
@ -1,47 +0,0 @@
|
||||
import { nip19 } from "nostr-tools";
|
||||
import puppeteer, { PuppeteerLaunchOptions } from "puppeteer";
|
||||
import { join } from "path";
|
||||
import pfs from "fs/promises";
|
||||
import { npubEncode } from "nostr-tools/nip19";
|
||||
|
||||
import { NSITE_PORT, SCREENSHOTS_DIR } from "./env.js";
|
||||
|
||||
try {
|
||||
await pfs.mkdir(SCREENSHOTS_DIR, { recursive: true });
|
||||
} catch (error) {}
|
||||
|
||||
export function getScreenshotPath(pubkey: string) {
|
||||
return join(SCREENSHOTS_DIR, pubkey + ".png");
|
||||
}
|
||||
|
||||
export async function hasScreenshot(pubkey: string) {
|
||||
try {
|
||||
await pfs.stat(getScreenshotPath(pubkey));
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function takeScreenshot(pubkey: string) {
|
||||
console.log(`${npubEncode(pubkey)}: Generating screenshot`);
|
||||
|
||||
const opts: PuppeteerLaunchOptions = {
|
||||
args: ["--no-sandbox"],
|
||||
};
|
||||
if (process.env.PUPPETEER_SKIP_DOWNLOAD) opts.executablePath = "google-chrome-stable";
|
||||
|
||||
const browser = await puppeteer.launch(opts);
|
||||
const page = await browser.newPage();
|
||||
const url = new URL(`http://${nip19.npubEncode(pubkey)}.localhost:${NSITE_PORT}`);
|
||||
await page.goto(url.toString());
|
||||
await page.screenshot({ path: getScreenshotPath(pubkey) });
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
export async function removeScreenshot(pubkey: string) {
|
||||
try {
|
||||
await pfs.rm(getScreenshotPath(pubkey));
|
||||
console.log(`${npubEncode(pubkey)}: Removed screenshot`);
|
||||
} catch (error) {}
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
[supervisord]
|
||||
nodaemon=true
|
||||
user=root
|
||||
|
||||
[program:nginx]
|
||||
command=nginx -g "daemon off;"
|
||||
autostart=true
|
||||
autorestart=true
|
||||
stdout_logfile=/dev/stdout
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile=/dev/stderr
|
||||
stderr_logfile_maxbytes=0
|
||||
|
||||
[program:nsite]
|
||||
user=nsite
|
||||
group=nsite
|
||||
command=node /app
|
||||
autostart=true
|
||||
autorestart=true
|
||||
stdout_logfile=/dev/stdout
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile=/dev/stderr
|
||||
stderr_logfile_maxbytes=0
|
Loading…
x
Reference in New Issue
Block a user