Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions development/redeploy_host.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#!/bin/sh
# SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
# SPDX-License-Identifier: AGPL-3.0-or-later

# This file can be used for development for the "manual install" deployment type when FRP is disabled.
# For Julius Docker-Dev, you need to additionally edit the `data/nginx/vhost.d/nextcloud.local_location` file,
# changing `appapi-harp` to `172.17.0.1` and restart the "proxy" container.

docker container remove --force appapi-harp

docker build -t nextcloud-appapi-harp:local .

docker run \
-e HP_SHARED_KEY="some_very_secure_password" \
-e NC_INSTANCE_URL="http://nextcloud.local" \
-e HP_LOG_LEVEL="info" \
-e HP_VERBOSE_START="1" \
-v /var/run/docker.sock:/var/run/docker.sock \
-v `pwd`/certs:/certs \
--name appapi-harp -h appapi-harp \
--restart unless-stopped \
--network=host \
-d nextcloud-appapi-harp:local
6 changes: 4 additions & 2 deletions haproxy.cfg.template
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,9 @@ _HTTPS_FRONTEND_ use_backend %[var(txn.exapps.backend)]
###############################################################################
backend ex_apps_backend
mode http
server frp_server 127.0.0.1
server frp_server 0.0.0.0
http-request set-path %[var(txn.exapps.target_path)]
http-request set-dst var(txn.exapps.target_ip)
http-request set-dst-port var(txn.exapps.target_port)
http-request set-header EX-APP-ID %[var(txn.exapps.exapp_id)]
http-request set-header EX-APP-VERSION %[var(txn.exapps.exapp_version)]
Expand All @@ -72,8 +73,9 @@ backend ex_apps_backend

backend ex_apps_backend_w_bruteforce
mode http
server frp_server 127.0.0.1
server frp_server 0.0.0.0
http-request set-path %[var(txn.exapps.target_path)]
http-request set-dst var(txn.exapps.target_ip)
http-request set-dst-port var(txn.exapps.target_port)
http-request set-header EX-APP-ID %[var(txn.exapps.exapp_id)]
http-request set-header EX-APP-VERSION %[var(txn.exapps.exapp_version)]
Expand Down
42 changes: 39 additions & 3 deletions haproxy_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,17 @@
# SPDX-License-Identifier: AGPL-3.0-or-later

import asyncio
import contextlib
import ipaddress
import json
import logging
import os
import re
import socket
import time
from base64 import b64encode
from enum import IntEnum
from ipaddress import IPv4Address, IPv6Address
from ipaddress import IPv4Address, IPv6Address, ip_address
from typing import Self

import aiohttp
Expand Down Expand Up @@ -69,8 +71,10 @@ def encode_bruteforce_protection_values(self) -> Self:
class ExApp(BaseModel):
exapp_token: str = Field(...)
exapp_version: str = Field(...)
host: str = Field(...)
port: int = Field(...)
routes: list[ExAppRoute] = Field([])
resolved_host: str = Field("", description="Contains resolved host field to the IP address.")


class NcUser(BaseModel):
Expand Down Expand Up @@ -200,7 +204,14 @@ async def exapps_msg(
exapp_record = None
if all(
key in request_headers
for key in ["ex-app-version", "ex-app-id", "ex-app-port", "authorization-app-api", "harp-shared-key"]
for key in [
"ex-app-version",
"ex-app-id",
"ex-app-host",
"ex-app-port",
"authorization-app-api",
"harp-shared-key",
]
):
# This is a direct request from AppAPI to ExApp using AppAPI PHP functions "requestToExAppXXX"
if request_headers["harp-shared-key"] != SHARED_KEY:
Expand All @@ -209,6 +220,7 @@ async def exapps_msg(
exapp_record = ExApp(
exapp_token="",
exapp_version=request_headers["ex-app-version"],
host=request_headers["ex-app-host"],
port=int(request_headers["ex-app-port"]),
)
authorization_app_api = request_headers["authorization-app-api"]
Expand Down Expand Up @@ -290,7 +302,18 @@ async def exapps_msg(
else:
reply = reply.set_txn_var("backend", "ex_apps_backend")

LOGGER.info("Rerouting request to %s:%s", target_path, exapp_record.port)
if not exapp_record.resolved_host:
try:
ip_address(exapp_record.host)
exapp_record.resolved_host = exapp_record.host
except ValueError:
exapp_record.resolved_host = resolve_ip(exapp_record.host)
if not exapp_record.resolved_host:
LOGGER.error("Cannot resolve '%s' to IP address.", exapp_record.host)
return reply.set_txn_var("not_found", 1)

LOGGER.info("Rerouting request to %s:%s with path=%s", exapp_record.resolved_host, exapp_record.port, target_path)
reply = reply.set_txn_var("target_ip", exapp_record.resolved_host)
reply = reply.set_txn_var("target_port", exapp_record.port)
reply = reply.set_txn_var("exapp_token", authorization_app_api)
reply = reply.set_txn_var("exapp_version", exapp_record.exapp_version)
Expand Down Expand Up @@ -384,6 +407,19 @@ async def nc_get_user(app_id: str, all_headers: dict[str, str]) -> NcUser | None
return NcUser.model_validate(data)


def resolve_ip(hostname: str) -> str:
with contextlib.suppress(socket.gaierror):
addr_info = socket.getaddrinfo(hostname, None)
for family, _, _, _, sockaddr in addr_info:
if family == socket.AF_INET: # IPv4
return sockaddr[0]
# If no IPv4, return first IPv6
for family, _, _, _, sockaddr in addr_info:
if family == socket.AF_INET6: # IPv6
return sockaddr[0]
return ""


###############################################################################
# ExApp routes
###############################################################################
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ line-length = 120
target-version = "py312"
lint.select = ["A", "B", "C", "D", "E", "F", "G", "I", "S", "SIM", "PIE", "Q", "RET", "RUF", "UP" , "W"]
lint.extend-ignore = ["D101", "D102", "D103", "D105", "D107", "D203", "D213", "D401", "I001", "RUF100", "D400", "D415"]
lint.mccabe.max-complexity = 28
lint.mccabe.max-complexity = 29
lint.extend-per-file-ignores."development/**/*.py" = [
"D",
"S",
Expand Down