mirror of
https://github.com/alangrainger/immich-public-proxy.git
synced 2025-01-16 04:46:45 +01:00
chore: merge upstream
This commit is contained in:
commit
9c85edbf76
9 changed files with 110 additions and 4601 deletions
|
@ -1,4 +1,3 @@
|
||||||
IMMICH_URL=http://localhost:2283
|
IMMICH_URL=http://localhost:2283
|
||||||
API_KEY="Get this from your Immich Account Settings page"
|
|
||||||
PORT=3000
|
PORT=3000
|
||||||
CACHE_AGE=2592000
|
CACHE_AGE=2592000
|
||||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -2,3 +2,4 @@
|
||||||
/dist/
|
/dist/
|
||||||
/.idea/
|
/.idea/
|
||||||
.env
|
.env
|
||||||
|
/package-lock.json
|
||||||
|
|
25
README.md
25
README.md
|
@ -15,9 +15,16 @@ those shared images.
|
||||||
|
|
||||||
It exposes no ports, allows no incoming data, and has no API to exploit.
|
It exposes no ports, allows no incoming data, and has no API to exploit.
|
||||||
|
|
||||||
|
[Live demo](https://immich-demo.note.sx/share/ffSw63qnIYMtpmg0RNvOui0Dpio7BbxsObjvH8YZaobIjIAzl5n7zTX5d6EDHdOYEvo)
|
||||||
|
|
||||||
|
### Why not simply put Immich behind a reverse proxy and only expose the `/share/` path to the public?
|
||||||
|
|
||||||
|
To view a shared album in Immich, you need access to the `/api/` path. If you're sharing a gallery with the public, you need
|
||||||
|
to make that path public. Any existing or future vulnerabilities could compromise your Immich instance.
|
||||||
|
|
||||||
The ideal setup is to have Immich secured privately behind VPN or mTLS, and only allow public access to Immich Public Proxy.
|
The ideal setup is to have Immich secured privately behind VPN or mTLS, and only allow public access to Immich Public Proxy.
|
||||||
|
|
||||||
[Live demo](https://immich-demo.note.sx/share/ffSw63qnIYMtpmg0RNvOui0Dpio7BbxsObjvH8YZaobIjIAzl5n7zTX5d6EDHdOYEvo)
|
Here is an example setup for [securing Immich behind mTLS](./docs/securing-immich-with-mtls.md).
|
||||||
|
|
||||||
## How to install with Docker
|
## How to install with Docker
|
||||||
|
|
||||||
|
@ -31,13 +38,11 @@ git clone https://github.com/alangrainger/immich-public-proxy.git
|
||||||
|
|
||||||
```
|
```
|
||||||
IMMICH_URL=http://localhost:2283
|
IMMICH_URL=http://localhost:2283
|
||||||
API_KEY="Get this from your Immich Account Settings page"
|
|
||||||
PORT=3000
|
PORT=3000
|
||||||
CACHE_AGE=2592000
|
CACHE_AGE=2592000
|
||||||
```
|
```
|
||||||
|
|
||||||
- `IMMICH_URL` is the URL to access Immich in your local network. This is not your public URL.
|
- `IMMICH_URL` is the URL to access Immich in your local network. This is not your public URL.
|
||||||
- `API_KEY` get this from the Account Settings page of your Immich user account.
|
|
||||||
- `CACHE_AGE` this is setting the `cache-control` header, to tell the browser to cache the assets. Set to 0 to disable caching.
|
- `CACHE_AGE` this is setting the `cache-control` header, to tell the browser to cache the assets. Set to 0 to disable caching.
|
||||||
|
|
||||||
3. Start the docker container:
|
3. Start the docker container:
|
||||||
|
@ -48,7 +53,7 @@ docker-compose up -d
|
||||||
|
|
||||||
4. Set the "External domain" in your Immich **Server Settings** to be the same as the public URL for your Immich Public Proxy:
|
4. Set the "External domain" in your Immich **Server Settings** to be the same as the public URL for your Immich Public Proxy:
|
||||||
|
|
||||||
<img src="public/images/server-settings.png" width="418" height="205">
|
<img src="public/images/server-settings.png" width="400" height="182">
|
||||||
|
|
||||||
Now whenever you share an image or gallery through Immich, it will automatically create the
|
Now whenever you share an image or gallery through Immich, it will automatically create the
|
||||||
correct public path for you.
|
correct public path for you.
|
||||||
|
@ -64,15 +69,15 @@ When the proxy receives a request, it will come as a link like this:
|
||||||
https://your-proxy-url.com/share/ffSw63qnIYMtpmg0RNvOui0Dpio7BbxsObjvH8YZaobIjIAzl5n7zTX5d6EDHdOYEvo
|
https://your-proxy-url.com/share/ffSw63qnIYMtpmg0RNvOui0Dpio7BbxsObjvH8YZaobIjIAzl5n7zTX5d6EDHdOYEvo
|
||||||
```
|
```
|
||||||
|
|
||||||
The part after `/share/` is Immich's shared link public ID (called the `key` [in the docs](https://immich.app/docs/api/get-all-shared-links/)).
|
The part after `/share/` is Immich's shared link public ID (called the `key` [in the docs](https://immich.app/docs/api/get-my-shared-link)).
|
||||||
|
|
||||||
**Immich Public Proxy** takes that key and makes an API call to your Immich instance over your local network, to ask what
|
**Immich Public Proxy** takes that key and makes an API call to your Immich instance over your local network, to ask what
|
||||||
photos or videos are shared in that share URL.
|
photos or videos are shared in that share URL.
|
||||||
|
|
||||||
If it is a valid share URL, the proxy fetches just those assets via local API and returns them to the visitor as an
|
If it is a valid share URL, the proxy fetches just those assets via local API and returns them to the visitor as an
|
||||||
individual image or gallery.
|
individual image or gallery.
|
||||||
|
|
||||||
If the shared link is expired or any of the assets have been put in the Immich trash, it will not return those.
|
If the shared link has expired or any of the assets have been put in the Immich trash, it will not return those.
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
|
@ -103,7 +108,9 @@ https://www.lightgalleryjs.com/docs/settings/
|
||||||
## Feature requests
|
## Feature requests
|
||||||
|
|
||||||
You can [add feature requests here](https://github.com/alangrainger/immich-public-proxy/discussions/categories/feature-requests?discussions_q=is%3Aopen+category%3A%22Feature+Requests%22+sort%3Atop),
|
You can [add feature requests here](https://github.com/alangrainger/immich-public-proxy/discussions/categories/feature-requests?discussions_q=is%3Aopen+category%3A%22Feature+Requests%22+sort%3Atop),
|
||||||
however my goal with this project is to keep it as lean as possible.
|
however my goal with this project is to keep it as lean as possible.
|
||||||
|
|
||||||
Due to the sensitivity of data contained within Immich, I want anyone with a bit of coding knowledge
|
Due to the sensitivity of data contained within Immich, I want anyone with a bit of coding knowledge
|
||||||
to be able to read this codebase and fully understand everything it is doing.
|
to be able to read this codebase and fully understand everything it is doing.
|
||||||
|
|
||||||
|
## D
|
||||||
|
|
72
docs/securing-immich-with-mtls.md
Normal file
72
docs/securing-immich-with-mtls.md
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
# Securing Immich with mTLS using Caddy
|
||||||
|
|
||||||
|
## Caddy docker-compose.yml
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
version: "3.7"
|
||||||
|
|
||||||
|
services:
|
||||||
|
caddy:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
restart: unless-stopped
|
||||||
|
cap_add:
|
||||||
|
- NET_ADMIN
|
||||||
|
ports:
|
||||||
|
- "6443:443"
|
||||||
|
- "6443:443/udp"
|
||||||
|
volumes:
|
||||||
|
- ./Caddyfile:/etc/caddy/Caddyfile:Z
|
||||||
|
- ./site:/srv:Z
|
||||||
|
- ./data:/data:Z
|
||||||
|
- ./config:/config:Z
|
||||||
|
```
|
||||||
|
|
||||||
|
## Authenticating with mutual TLS
|
||||||
|
|
||||||
|
### Generate your client certificate
|
||||||
|
|
||||||
|
This is a basic way to generate a certificate for a user. If it's only you using your own homelab, then you'll just need to make one certificate.
|
||||||
|
|
||||||
|
This certificate will last for ~10 years (although of course you can revoke it at any time by deleting it from Caddy's key store).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
mkdir -p certs
|
||||||
|
|
||||||
|
# Generate CA certificates
|
||||||
|
openssl genrsa -out certs/client-ca.key 4096
|
||||||
|
openssl req -new -x509 -nodes -days 3600 -key certs/client-ca.key -out certs/client-ca.crt
|
||||||
|
|
||||||
|
# Generate a certificate signing request
|
||||||
|
openssl req -newkey rsa:4096 -nodes -keyout certs/client.key -out certs/client.req
|
||||||
|
|
||||||
|
# Have the CA sign the certificate requests and output the certificates.
|
||||||
|
openssl x509 -req -in certs/client.req -days 3600 -CA certs/client-ca.crt -CAkey certs/client-ca.key -set_serial 01 -out certs/client.crt
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "Please enter a STRONG password. Many clients *require* a password for you to be able to import the certificate, and you want to protect it."
|
||||||
|
echo
|
||||||
|
|
||||||
|
# Convert the cerificate to PKCS12 format (for import into browser)
|
||||||
|
openssl pkcs12 -export -out certs/client.pfx -inkey certs/client.key -in certs/client.crt
|
||||||
|
|
||||||
|
# Clean up
|
||||||
|
rm certs/client.req
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configure Caddyfile
|
||||||
|
|
||||||
|
```Caddyfile
|
||||||
|
https://immich.mydomain.com {
|
||||||
|
tls {
|
||||||
|
client_auth {
|
||||||
|
mode require_and_verify
|
||||||
|
trusted_ca_cert_file /data/client_certs/client.crt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reverse_proxy internal_server.lan:2283
|
||||||
|
}
|
||||||
|
```
|
4575
package-lock.json
generated
4575
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "immich-public-proxy",
|
"name": "immich-public-proxy",
|
||||||
"version": "1.0.0",
|
"version": "1.1.1",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "ts-node src/index.ts",
|
"dev": "ts-node src/index.ts",
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 12 KiB |
|
@ -1,4 +1,4 @@
|
||||||
import { Asset, AssetType, ImageSize, SharedLink } from './types'
|
import { Album, Asset, AssetType, ImageSize, SharedLink } from './types'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { log } from './index'
|
import { log } from './index'
|
||||||
|
|
||||||
|
@ -8,11 +8,7 @@ class Immich {
|
||||||
* the possible attack surface of this app.
|
* the possible attack surface of this app.
|
||||||
*/
|
*/
|
||||||
async request (endpoint: string) {
|
async request (endpoint: string) {
|
||||||
const res = await fetch(process.env.IMMICH_URL + '/api' + endpoint, {
|
const res = await fetch(process.env.IMMICH_URL + '/api' + endpoint)
|
||||||
headers: {
|
|
||||||
'x-api-key': process.env.API_KEY || ''
|
|
||||||
}
|
|
||||||
})
|
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
const contentType = res.headers.get('Content-Type') || ''
|
const contentType = res.headers.get('Content-Type') || ''
|
||||||
if (contentType.includes('application/json')) {
|
if (contentType.includes('application/json')) {
|
||||||
|
@ -20,26 +16,26 @@ class Immich {
|
||||||
} else {
|
} else {
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
log('Immich API status ' + res.status)
|
||||||
|
console.log(await res.text())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Query Immich for the SharedLink metadata for a given key.
|
* Query Immich for the SharedLink metadata for a given key.
|
||||||
* The key is what is returned in the URL when you create a share in Immich.
|
* The key is what is returned in the URL when you create a share in Immich.
|
||||||
*
|
|
||||||
* Immich doesn't have a method to query by key, so this method gets all
|
|
||||||
* known shared links, and returns the link which matches the provided key.
|
|
||||||
*/
|
*/
|
||||||
async getShareByKey (key: string) {
|
async getShareByKey (key: string) {
|
||||||
const res = (await this.request('/shared-links') || []) as SharedLink[]
|
const link = (await this.request('/shared-links/me?key=' + encodeURIComponent(key))) as SharedLink
|
||||||
const link = res.find(x => x.key === key)
|
|
||||||
if (link) {
|
if (link) {
|
||||||
// Filter assets to exclude trashed assets
|
|
||||||
link.assets = link.assets.filter(x => !x.isTrashed)
|
|
||||||
if (link.expiresAt && dayjs(link.expiresAt) < dayjs()) {
|
if (link.expiresAt && dayjs(link.expiresAt) < dayjs()) {
|
||||||
// This link has expired
|
// This link has expired
|
||||||
log('Expired link ' + key)
|
log('Expired link ' + key)
|
||||||
} else {
|
} else {
|
||||||
|
// Filter assets to exclude trashed assets
|
||||||
|
link.assets = link.assets.filter(asset => !asset.isTrashed)
|
||||||
|
link.assets.forEach(asset => { asset.key = key })
|
||||||
return link
|
return link
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -55,9 +51,9 @@ class Immich {
|
||||||
switch (asset.type) {
|
switch (asset.type) {
|
||||||
case AssetType.image:
|
case AssetType.image:
|
||||||
size = size === ImageSize.thumbnail ? ImageSize.thumbnail : ImageSize.original
|
size = size === ImageSize.thumbnail ? ImageSize.thumbnail : ImageSize.original
|
||||||
return this.request('/assets/' + encodeURIComponent(asset.id) + '/' + size)
|
return this.request('/assets/' + encodeURIComponent(asset.id) + '/' + size + '?key=' + encodeURIComponent(asset.key))
|
||||||
case AssetType.video:
|
case AssetType.video:
|
||||||
return this.request('/assets/' + encodeURIComponent(asset.id) + '/video/playback')
|
return this.request('/assets/' + encodeURIComponent(asset.id) + '/video/playback?key=' + encodeURIComponent(asset.key))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,16 +5,25 @@ export enum AssetType {
|
||||||
|
|
||||||
export interface Asset {
|
export interface Asset {
|
||||||
id: string;
|
id: string;
|
||||||
|
key: string;
|
||||||
type: AssetType;
|
type: AssetType;
|
||||||
isTrashed: boolean;
|
isTrashed: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SharedLink {
|
export interface SharedLink {
|
||||||
key: string;
|
key: string;
|
||||||
|
type: string;
|
||||||
assets: Asset[];
|
assets: Asset[];
|
||||||
|
album?: {
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
expiresAt: string | null;
|
expiresAt: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Album {
|
||||||
|
assets: Asset[]
|
||||||
|
}
|
||||||
|
|
||||||
export enum ImageSize {
|
export enum ImageSize {
|
||||||
thumbnail = 'thumbnail',
|
thumbnail = 'thumbnail',
|
||||||
original = 'original'
|
original = 'original'
|
||||||
|
|
Loading…
Reference in a new issue