1
0
Fork 0
mirror of https://github.com/alangrainger/immich-public-proxy.git synced 2024-12-28 03:41:58 +00:00

chore: merge upstream

This commit is contained in:
niklastasler@gmail.com 2024-10-31 08:13:13 +01:00
commit 9c85edbf76
9 changed files with 110 additions and 4601 deletions

View file

@ -1,4 +1,3 @@
IMMICH_URL=http://localhost:2283
API_KEY="Get this from your Immich Account Settings page"
PORT=3000
CACHE_AGE=2592000

1
.gitignore vendored
View file

@ -2,3 +2,4 @@
/dist/
/.idea/
.env
/package-lock.json

View file

@ -15,9 +15,16 @@ those shared images.
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.
[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
@ -31,13 +38,11 @@ git clone https://github.com/alangrainger/immich-public-proxy.git
```
IMMICH_URL=http://localhost:2283
API_KEY="Get this from your Immich Account Settings page"
PORT=3000
CACHE_AGE=2592000
```
- `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.
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:
<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
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
```
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.
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.
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
@ -103,7 +108,9 @@ https://www.lightgalleryjs.com/docs/settings/
## 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),
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
to be able to read this codebase and fully understand everything it is doing.
## D

View 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

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
{
"name": "immich-public-proxy",
"version": "1.0.0",
"version": "1.1.1",
"main": "dist/index.js",
"scripts": {
"dev": "ts-node src/index.ts",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View file

@ -1,4 +1,4 @@
import { Asset, AssetType, ImageSize, SharedLink } from './types'
import { Album, Asset, AssetType, ImageSize, SharedLink } from './types'
import dayjs from 'dayjs'
import { log } from './index'
@ -8,11 +8,7 @@ class Immich {
* the possible attack surface of this app.
*/
async request (endpoint: string) {
const res = await fetch(process.env.IMMICH_URL + '/api' + endpoint, {
headers: {
'x-api-key': process.env.API_KEY || ''
}
})
const res = await fetch(process.env.IMMICH_URL + '/api' + endpoint)
if (res.status === 200) {
const contentType = res.headers.get('Content-Type') || ''
if (contentType.includes('application/json')) {
@ -20,26 +16,26 @@ class Immich {
} else {
return res
}
} else {
log('Immich API status ' + res.status)
console.log(await res.text())
}
}
/**
* 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.
*
* 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) {
const res = (await this.request('/shared-links') || []) as SharedLink[]
const link = res.find(x => x.key === key)
const link = (await this.request('/shared-links/me?key=' + encodeURIComponent(key))) as SharedLink
if (link) {
// Filter assets to exclude trashed assets
link.assets = link.assets.filter(x => !x.isTrashed)
if (link.expiresAt && dayjs(link.expiresAt) < dayjs()) {
// This link has expired
log('Expired link ' + key)
} else {
// Filter assets to exclude trashed assets
link.assets = link.assets.filter(asset => !asset.isTrashed)
link.assets.forEach(asset => { asset.key = key })
return link
}
}
@ -55,9 +51,9 @@ class Immich {
switch (asset.type) {
case AssetType.image:
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:
return this.request('/assets/' + encodeURIComponent(asset.id) + '/video/playback')
return this.request('/assets/' + encodeURIComponent(asset.id) + '/video/playback?key=' + encodeURIComponent(asset.key))
}
}

View file

@ -5,16 +5,25 @@ export enum AssetType {
export interface Asset {
id: string;
key: string;
type: AssetType;
isTrashed: boolean;
}
export interface SharedLink {
key: string;
type: string;
assets: Asset[];
album?: {
id: string;
}
expiresAt: string | null;
}
export interface Album {
assets: Asset[]
}
export enum ImageSize {
thumbnail = 'thumbnail',
original = 'original'