Compare commits

..

No commits in common. "master" and "min-libconductor" have entirely different histories.

40 changed files with 269 additions and 1407 deletions

View file

@ -1,3 +0,0 @@
/target
.env.local
tarpaulin-report.html

View file

@ -7,17 +7,6 @@ pipeline:
# - make migrate
- make
- make test
- make release
publish_bins:
image: rust
commands:
- apt update
- apt-get -y --no-install-recommends install gpg tar curl wget
- echo -n "$RELEASE_BOT_GPG_SIGNING_KEY" | gpg --batch --import --pinentry-mode loopback
- ./scripts/bin-publish.sh publish master latest $DUMBSERVE_PASSWORD
secrets: [ RELEASE_BOT_GPG_SIGNING_KEY, DUMBSERVE_PASSWORD, GPG_PASSWORD ]
publish:
image: plugins/docker
@ -25,7 +14,7 @@ pipeline:
username: realaravinth
password:
from_secret: DOCKER_TOKEN
repo: realaravinth/librepages-conductor
repo: realaravinth/librepages-forms
tags: latest
#services:

701
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -12,23 +12,18 @@ build = "build.rs"
[dependencies]
actix-web = "4"
actix-web-prom = "0.6.0"
futures-util = { version = "0.3.17", default-features = false, features = ["std"] }
lazy_static = "1.4.0"
log = "0.4.17"
pretty_env_logger = "0.4.0"
serde = { version = "1", features=["derive"]}
actix-web-codegen-const-routes = { version = "0.1.0", tag = "0.1.0", git = "https://github.com/realaravinth/actix-web-codegen-const-routes" }
libconfig = { version = "0.1.0", git = "https://git.batsense.net/librepages/libconfig" }
derive_builder = "0.11.2"
config = "0.13"
config = "0.11"
derive_more = "0.99.17"
url = { version = "2.2.2", features = ["serde"]}
serde_json = { version ="1", features = ["raw_value"]}
clap = { vesrion = "3.2.20", features = ["derive"]}
actix-web-httpauth = "0.8.0"
mime_guess = "2.0.4"
rust-embed = "6.4.2"
[dependencies.libconductor]
path = "./env/libconductor"

View file

@ -1,12 +1,3 @@
define lint
cargo fmt -v --all -- --emit files
cargo clippy --workspace --tests --all-features
endef
define test
cargo test --no-fail-fast --workspace --tests --all-features
endef
default: ## Build app in debug mode
cargo build
@ -34,9 +25,8 @@ env: ## Setup development environtment
cargo fetch
lint: ## Lint codebase
$(call lint)
cd env/dummy_conductor && $(call lint)
cd env/libconductor && $(call lint)
cargo fmt -v --all -- --emit files
cargo clippy --workspace --tests --all-features
#migrate: ## run migrations
# unset DATABASE_URL && cargo build
@ -46,7 +36,7 @@ release: ## Build app with release optimizations
cargo build --release
run: ## Run app in debug mode
cargo run -- serve
cargo run
#sqlx-offline-data: ## prepare sqlx offline data
@ -55,8 +45,7 @@ run: ## Run app in debug mode
# --all-features
test: ## Run all available tests
$(call test)
cd env/dummy_conductor && $(call test)
cargo test --no-fail-fast --workspace
xml-test-coverage: ## Generate code coverage report in XML format
cargo tarpaulin -t 1200 --out Xml

View file

@ -2,9 +2,6 @@ debug = true
source_code = "https://git.batsense.net/librepages/conductor"
conductor = "dummy"
[creds]
token="longrandomlygeneratedpassword"
[server]
# Please set a unique value, your mCaptcha instance's security depends on this being
# unique
@ -13,7 +10,7 @@ token="longrandomlygeneratedpassword"
port = 7000
#IP address. Enter 0.0.0.0 to listen on all available addresses
#ip= "0.0.0.0"
ip= "127.0.0.1"
ip= "192.168.0.104"
# enter your hostname, eg: example.com
domain = "localhost"
# Set true if you have setup TLS with a reverse proxy like Nginx.

View file

@ -1,24 +0,0 @@
[Unit]
Description=LibrePages Conductor: Easiest way to deploy websites. Conductor component
[Service]
Type=simple
User=root
ExecStart=/usr/bin/conductor serve
Restart=on-failure
RestartSec=1
SuccessExitStatus=3 4
RestartForceExitStatus=3 4
SystemCallArchitectures=native
MemoryDenyWriteExecute=true
NoNewPrivileges=true
Environment="RUST_LOG=info"
[Unit]
Wants=network-online.target
Wants=network-online.target
Requires=postgresql.service
After=syslog.target
[Install]
WantedBy=multi-user.target

Binary file not shown.

Before

Width:  |  Height:  |  Size: 665 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 628 B

View file

@ -1,16 +0,0 @@
html {
box-sizing: border-box;
overflow: -moz-scrollbars-vertical;
overflow-y: scroll;
}
*,
*:before,
*:after {
box-sizing: inherit;
}
body {
margin: 0;
background: #fafafa;
}

View file

@ -1,19 +0,0 @@
<!-- HTML for static distribution bundle build -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>LibrePages Conductor | Swagger UI</title>
<link rel="stylesheet" type="text/css" href="/docs/openapi/swagger-ui.css" />
<link rel="stylesheet" type="text/css" href="/docs/openapi/index.css" />
<link rel="icon" type="image/png" href="/docs/openapi/favicon-32x32.png" sizes="32x32" />
<link rel="icon" type="image/png" href="/docs/openapi/favicon-16x16.png" sizes="16x16" />
</head>
<body>
<div id="swagger-ui"></div>
<script src="/docs/openapi/swagger-ui-bundle.js" charset="UTF-8"> </script>
<script src="/docs/openapi/swagger-ui-standalone-preset.js" charset="UTF-8"> </script>
<script src="/docs/openapi/swagger-initializer.js" charset="UTF-8"> </script>
</body>
</html>

View file

@ -1,79 +0,0 @@
<!doctype html>
<html lang="en-US">
<head>
<title>Swagger UI: OAuth2 Redirect</title>
</head>
<body>
<script>
'use strict';
function run () {
var oauth2 = window.opener.swaggerUIRedirectOauth2;
var sentState = oauth2.state;
var redirectUrl = oauth2.redirectUrl;
var isValid, qp, arr;
if (/code|token|error/.test(window.location.hash)) {
qp = window.location.hash.substring(1).replace('?', '&');
} else {
qp = location.search.substring(1);
}
arr = qp.split("&");
arr.forEach(function (v,i,_arr) { _arr[i] = '"' + v.replace('=', '":"') + '"';});
qp = qp ? JSON.parse('{' + arr.join() + '}',
function (key, value) {
return key === "" ? value : decodeURIComponent(value);
}
) : {};
isValid = qp.state === sentState;
if ((
oauth2.auth.schema.get("flow") === "accessCode" ||
oauth2.auth.schema.get("flow") === "authorizationCode" ||
oauth2.auth.schema.get("flow") === "authorization_code"
) && !oauth2.auth.code) {
if (!isValid) {
oauth2.errCb({
authId: oauth2.auth.name,
source: "auth",
level: "warning",
message: "Authorization may be unsafe, passed state was changed in server. The passed state wasn't returned from auth server."
});
}
if (qp.code) {
delete oauth2.state;
oauth2.auth.code = qp.code;
oauth2.callback({auth: oauth2.auth, redirectUrl: redirectUrl});
} else {
let oauthErrorMsg;
if (qp.error) {
oauthErrorMsg = "["+qp.error+"]: " +
(qp.error_description ? qp.error_description+ ". " : "no accessCode received from the server. ") +
(qp.error_uri ? "More info: "+qp.error_uri : "");
}
oauth2.errCb({
authId: oauth2.auth.name,
source: "auth",
level: "error",
message: oauthErrorMsg || "[Authorization failed]: no accessCode received from the server."
});
}
} else {
oauth2.callback({auth: oauth2.auth, token: qp, isValid: isValid, redirectUrl: redirectUrl});
}
window.close();
}
if (document.readyState !== 'loading') {
run();
} else {
document.addEventListener('DOMContentLoaded', function () {
run();
});
}
</script>
</body>
</html>

View file

@ -1,178 +0,0 @@
openapi: 3.0.3
info:
title: LibrePages Conductor - OpenAPI 3.0
description: |-
Conductor is the deployment manager used internally in LibrePages. It is
responsible for creating, updating and deleting websites that are deployed
with LibrePages
Some useful links:
- [LibrePages Conductor repository](https://git.batsense.net/LibrePages/conductor)
termsOfService: http://libreapages.org/terms/
contact:
email: contact@libreapages.org
license:
name: AGPLv3 or later version
url: https://www.gnu.org/licenses/agpl.html
version: 0.1.0
externalDocs:
description: LibrePages Conductor - internal service to update deployments
url: http://git.batsense.net/LibrePages/conductor
tags:
- name: meta
description: Information about the system
- name: site
description: Information about customer site deployments
paths:
/api/v1/events/new:
post:
tags:
- site
summary: Post new event to Conductor
description: Conductor schedules jobs based on events posted to it.
operationId: eventsNew
responses:
"201":
description: Successful operation
content:
application/json:
schema:
oneOf:
- $ref: "#/components/schemas/eventsNewPayloadNewSite"
- $ref: "#/components/schemas/eventsNewPayloadConfig"
- $ref: "#/components/schemas/eventsNewPayloadDeleteSite"
/api/v1/meta/build:
get:
tags:
- meta
summary: Get binary's build information
description: Update an existing pet by Idinformation
operationId: metaBuild
responses:
"200":
description: Successful operation
content:
application/json:
schema:
$ref: "#/components/schemas/BuildInformation"
/api/v1/meta/health:
get:
tags:
- meta
summary: Get instance's health information
description: Get instance's health information
operationId: metaHealth
responses:
"200":
description: Successful operation
content:
application/json:
schema:
$ref: "#/components/schemas/HealthInformation"
components:
schemas:
BuildInformation:
required:
- version
- git_commit_hash
- source_code
type: object
properties:
version:
type: string
example: v0.1.0
git_commit_hash:
type: string
example: 1fa28ef9b70bb04d6c76eee9e9bc5be77005b4b0
source_code:
type: string
example: https://git.batsense.net/LibrePages
HealthInformation:
required:
- conductor
type: object
properties:
conductor:
type: boolean
example: true
eventsNewPayloadConfig:
type: object
required:
- data
properties:
data:
$ref: '#/components/schemas/LibConfigConfig'
LibConfigConfig:
properties:
source:
$ref: '#/components/schemas/LibConfigSource'
forms:
$ref: '#/components/schemas/LibConfigForms'
domains:
type: array
items:
type: string
example: ["example.com", "testing.example.org"]
image_compression:
$ref: '#/components/schemas/LibConfigImageCompression'
redirects:
$ref: '#/components/schemas/LibConfigRedirects'
LibConfigSource:
properties:
production_branch:
type: string
example: "librepages"
staging_branch:
type: string
example: "librepages-staging"
LibConfigForms:
properties:
enabled:
type: boolean
example: false
LibConfigImageCompression:
properties:
enabled:
type: boolean
example: false
LibConfigRedirects:
properties:
from:
type: string
example: "/from"
to:
type: string
example: "/to"
eventsNewPayloadNewSite:
properties:
hostname:
type: string
example: "example.org"
path:
type: string
example: "/tmp/example.org"
branch:
type: string
example: "librepages"
eventsNewPayloadDeleteSite:
properties:
hostname:
type: string
example: "example.org"
securitySchemes:
basicAuth:
type: http
scheme: basic

View file

@ -1,20 +0,0 @@
window.onload = function() {
//<editor-fold desc="Changeable Configuration Block">
// the following lines will be replaced by docker/configurator, when it runs in a docker-container
window.ui = SwaggerUIBundle({
url: "/docs/openapi/openapi.yml",
dom_id: '#swagger-ui',
deepLinking: true,
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
],
plugins: [
SwaggerUIBundle.plugins.DownloadUrl
],
layout: "StandaloneLayout"
});
//</editor-fold>
};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

128
env/dummy_conductor/Cargo.lock generated vendored
View file

@ -13,12 +13,6 @@ dependencies = [
"syn",
]
[[package]]
name = "autocfg"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "dummy_conductor"
version = "0.1.0"
@ -27,16 +21,6 @@ dependencies = [
"libconductor",
"serde",
"serde_json",
"tokio",
]
[[package]]
name = "hermit-abi"
version = "0.1.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
dependencies = [
"libc",
]
[[package]]
@ -45,46 +29,15 @@ version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c8af84674fe1f223a982c933a0ee1086ac4d4052aa0fb8060c12c6ad838e754"
[[package]]
name = "libc"
version = "0.2.138"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db6d7e329c562c5dfab7a46a2afabc8b987ab9a4834c9d1ca04dc54c1546cef8"
[[package]]
name = "libconductor"
version = "0.1.0"
dependencies = [
"async-trait",
"libconfig",
"serde",
"serde_json",
]
[[package]]
name = "libconfig"
version = "0.1.0"
source = "git+https://git.batsense.net/librepages/libconfig#f54290c4bae26b51a4945e0bf812e2b99856963b"
dependencies = [
"serde",
]
[[package]]
name = "num_cpus"
version = "1.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6058e64324c71e02bc2b150e4f3bc8286db6c83092132ffa3f6b1eab0f9def5"
dependencies = [
"hermit-abi",
"libc",
]
[[package]]
name = "pin-project-lite"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116"
[[package]]
name = "proc-macro2"
version = "1.0.46"
@ -151,89 +104,8 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "tokio"
version = "1.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eab6d665857cc6ca78d6e80303a02cea7a7851e85dfbd77cbdc09bd129f1ef46"
dependencies = [
"autocfg",
"num_cpus",
"pin-project-lite",
"tokio-macros",
"windows-sys",
]
[[package]]
name = "tokio-macros"
version = "1.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "unicode-ident"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcc811dc4066ac62f84f11307873c4850cb653bfa9b1719cee2bd2204a4bc5dd"
[[package]]
name = "windows-sys"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e"
[[package]]
name = "windows_aarch64_msvc"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4"
[[package]]
name = "windows_i686_gnu"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7"
[[package]]
name = "windows_i686_msvc"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246"
[[package]]
name = "windows_x86_64_gnu"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028"
[[package]]
name = "windows_x86_64_msvc"
version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5"

View file

@ -10,8 +10,5 @@ serde = { version = "1", features=["derive"]}
serde_json = { version ="1", features = ["raw_value"]}
async-trait = "0.1.57"
[dev-dependencies]
tokio = { version = "1.23.0", features = ["rt-multi-thread", "macros", "rt"] }
[dependencies.libconductor]
path = "../libconductor"

View file

@ -41,8 +41,8 @@ mod tests {
use super::*;
#[tokio::test]
async fn all_good() {
#[test]
fn all_good() {
let c = DummyConductor {};
assert_eq!(c.name(), DUMMY_CONDUCTOR_NAME);
assert!(c.health().await);

9
env/libconductor/Cargo.lock generated vendored
View file

@ -24,19 +24,10 @@ name = "libconductor"
version = "0.1.0"
dependencies = [
"async-trait",
"libconfig",
"serde",
"serde_json",
]
[[package]]
name = "libconfig"
version = "0.1.0"
source = "git+https://git.batsense.net/librepages/libconfig#f54290c4bae26b51a4945e0bf812e2b99856963b"
dependencies = [
"serde",
]
[[package]]
name = "proc-macro2"
version = "1.0.46"

View file

@ -9,7 +9,6 @@ edition = "2021"
serde = { version = "1", features=["derive"]}
serde_json = { version ="1", features = ["raw_value"]}
async-trait = { version = "0.1.57", optional = true}
libconfig = { version = "0.1.0", git = "https://git.batsense.net/librepages/libconfig" }
[features]
default = [

View file

@ -14,22 +14,10 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use libconfig::Config as LibConfig;
use serde::{Deserialize, Serialize};
#[derive(Deserialize, Serialize, Debug, PartialEq, Eq, Clone)]
#[serde(untagged)]
pub enum EventType {
NewSite {
path: String,
branch: String,
hostname: String,
},
DeleteSite {
hostname: String,
},
Config {
data: LibConfig,
},
NewHostname(String),
}

View file

@ -1,117 +0,0 @@
#!/bin/bash
# Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# publish.sh: grab bin from docker container, pack, sign and upload
# $2: binary version
# $3: Docker img tag
# $4: dumbserve password
set -xEeuo pipefail
DUMBSERVE_USERNAME=librepages
DUMBSERVE_PASSWORD=$4
DUMBSERVE_HOST="https://$DUMBSERVE_USERNAME:$DUMBSERVE_PASSWORD@dl.librepages.org"
NAME=conductor
KEY=67880CA5F4BC99BF247330E2DA576B07BC323961
TMP_DIR=$(mktemp -d)
FILENAME="$NAME-$2-linux-amd64"
TARBALL=$FILENAME.tar.gz
TARGET_DIR="$TMP_DIR/$FILENAME/"
mkdir -p $TARGET_DIR
DOCKER_IMG="realaravinth/$NAME:$3"
get_bin(){
cp target/release/conductor $TARGET_DIR
cp -r config/ $TARGET_DIR
cp -r contrib/ $TARGET_DIR
}
copy() {
echo "[*] Copying dist assets"
cp README.md $TARGET_DIR
cp LICENSE.md $TARGET_DIR
mkdir $TARGET_DIR/docs
# cp docs/CONFIGURATION.md $TARGET_DIR/docs
# cp -r docs/installation/ $TARGET_DIR/docs
get_bin
}
pack() {
echo "[*] Creating dist tarball"
pushd $TMP_DIR
tar -cvzf $TARBALL $FILENAME
popd
}
checksum() {
echo "[*] Generating dist tarball checksum"
pushd $TMP_DIR
sha256sum $TARBALL > $TARBALL.sha256
popd
}
sign() {
echo "[*] Signing dist tarball checksum"
pushd $TMP_DIR
export GPG_TTY=$(tty)
gpg --verbose \
--pinentry-mode loopback \
--batch --yes \
--passphrase $GPG_PASSWORD \
--local-user $KEY \
--output $TARBALL.asc \
--sign --detach \
--armor $TARBALL
popd
}
delete_dir() {
curl --location --request DELETE "$DUMBSERVE_HOST/api/v1/files/delete" \
--header 'Content-Type: application/json' \
--data-raw "{
\"path\": \"$1\"
}"
}
upload_dist() {
upload_dist="conductor/$1"
delete_dir $upload_dist
pushd $TMP_DIR
for file in $TARBALL $TARBALL.asc $TARBALL.sha256
do
curl -v \
-F upload=@$file \
"$DUMBSERVE_HOST/api/v1/files/upload?path=$upload_dist/"
done
popd
}
publish() {
copy
pack
checksum
sign
upload_dist $2
}
$1 $@

View file

@ -14,32 +14,11 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use actix_web::dev::ServiceRequest;
use actix_web::web;
use actix_web::Error;
use actix_web_httpauth::extractors::bearer::BearerAuth;
use crate::errors::*;
use crate::AppCtx;
use crate::SETTINGS;
pub mod meta;
pub mod webhook;
pub async fn bearerauth(
req: ServiceRequest,
credentials: BearerAuth,
) -> Result<ServiceRequest, (Error, ServiceRequest)> {
let _ctx: &AppCtx = req.app_data().unwrap();
let token = credentials.token();
if SETTINGS.authenticate(token) {
Ok(req)
} else {
let e = Error::from(ServiceError::Unauthorized);
Err((e, req))
}
}
pub const API_V1_ROUTES: routes::Routes = routes::Routes::new();
#[derive(Debug, Clone, Eq, PartialEq)]

View file

@ -15,7 +15,6 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use actix_web::{web, HttpResponse, Responder};
use actix_web_httpauth::middleware::HttpAuthentication;
use serde::{Deserialize, Serialize};
use libconductor::EventType;
@ -24,8 +23,6 @@ use crate::errors::*;
use crate::AppCtx;
use crate::*;
use super::bearerauth;
pub mod routes {
use super::*;
#[derive(Debug, Eq, PartialEq, Deserialize, Serialize)]
@ -45,10 +42,7 @@ pub fn services(cfg: &mut web::ServiceConfig) {
cfg.service(post_event);
}
#[actix_web_codegen_const_routes::post(
path = "API_V1_ROUTES.webhook.post_event",
wrap = "HttpAuthentication::bearer(bearerauth)"
)]
#[actix_web_codegen_const_routes::post(path = "API_V1_ROUTES.webhook.post_event")]
async fn post_event(ctx: AppCtx, payload: web::Json<EventType>) -> ServiceResult<impl Responder> {
ctx.conductor.process(payload.into_inner()).await;
Ok(HttpResponse::Created())
@ -56,9 +50,10 @@ async fn post_event(ctx: AppCtx, payload: web::Json<EventType>) -> ServiceResult
#[cfg(test)]
pub mod tests {
use super::*;
use actix_web::{http::StatusCode, test, App};
use super::*;
#[actix_rt::test]
async fn submit_works() {
let settings = Settings::new().unwrap();
@ -70,22 +65,14 @@ pub mod tests {
)
.await;
let creds = settings.creds.clone();
let auth = format!("Bearer {}", creds.token,);
let msg = EventType::NewSite {
hostname: "demo.librepages.org".into(),
branch: "librepages".into(),
path: "/tmp/librepages".into(),
};
let new_hostname = EventType::NewHostname("demo.librepages.org".into());
// upload json
let upload_json = test::call_service(
&app,
test::TestRequest::post()
.append_header((actix_web::http::header::AUTHORIZATION, auth.clone()))
.uri(API_V1_ROUTES.webhook.post_event)
.set_json(&msg)
.set_json(&new_hostname)
.to_request(),
)
.await;

View file

@ -37,9 +37,13 @@ impl Ctx {
pub async fn new(s: &Settings) -> ArcCtx {
let source_code = {
let mut url = s.source_code.clone();
url = url.join("tree/").unwrap();
url = url.join(crate::GIT_COMMIT_HASH).unwrap();
url.into()
if !url.ends_with('/') {
url.push('/');
}
let mut base = url::Url::parse(&url).unwrap();
base = base.join("tree/").unwrap();
base = base.join(crate::GIT_COMMIT_HASH).unwrap();
base.into()
};
let conductor: Box<dyn Conductor> = match s.conductor {

View file

@ -1,128 +0,0 @@
/*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use std::borrow::Cow;
use actix_web::body::BoxBody;
use actix_web::{http::header, web, HttpResponse, Responder};
use mime_guess::from_path;
use rust_embed::RustEmbed;
use crate::CACHE_AGE;
pub const DOCS: routes::Docs = routes::Docs::new();
pub mod routes {
pub struct Docs {
pub home: &'static str,
pub spec: &'static str,
pub assets: &'static str,
}
impl Docs {
pub const fn new() -> Self {
Docs {
home: "/docs/openapi",
spec: "/docs/openapi/openapi.yml",
assets: "/docs/openapi/{_:.*}",
}
}
}
}
pub fn services(cfg: &mut web::ServiceConfig) {
cfg.service(index).service(spec).service(dist);
}
#[derive(RustEmbed)]
#[folder = "docs/openapi/"]
struct Asset;
pub fn handle_embedded_file(path: &str) -> HttpResponse {
match Asset::get(path) {
Some(content) => {
let body: BoxBody = match content.data {
Cow::Borrowed(bytes) => BoxBody::new(bytes),
Cow::Owned(bytes) => BoxBody::new(bytes),
};
HttpResponse::Ok()
.insert_header(header::CacheControl(vec![
header::CacheDirective::Public,
header::CacheDirective::Extension("immutable".into(), None),
header::CacheDirective::MaxAge(CACHE_AGE),
]))
.content_type(from_path(path).first_or_octet_stream().as_ref())
.body(body)
}
None => HttpResponse::NotFound().body("404 Not Found"),
}
}
#[actix_web_codegen_const_routes::get(path = "DOCS.assets")]
async fn dist(path: web::Path<String>) -> impl Responder {
handle_embedded_file(&path)
}
const OPEN_API_SPEC: &str = include_str!("../docs/openapi/openapi.yml");
#[actix_web_codegen_const_routes::get(path = "DOCS.spec")]
async fn spec() -> HttpResponse {
HttpResponse::Ok()
.content_type("text/yaml")
.body(OPEN_API_SPEC)
}
#[actix_web_codegen_const_routes::get(path = "DOCS.home")]
async fn index() -> HttpResponse {
handle_embedded_file("index.html")
}
#[cfg(test)]
mod tests {
use actix_web::http::StatusCode;
use actix_web::test;
use super::*;
use crate::*;
#[actix_rt::test]
async fn docs_works() {
const FILE: &str = "openapi.yml";
let app = test::init_service(
App::new()
.wrap(actix_web::middleware::NormalizePath::new(
actix_web::middleware::TrailingSlash::Trim,
))
.configure(services),
)
.await;
let resp =
test::call_service(&app, test::TestRequest::get().uri(DOCS.home).to_request()).await;
assert_eq!(resp.status(), StatusCode::OK);
let resp =
test::call_service(&app, test::TestRequest::get().uri(DOCS.spec).to_request()).await;
assert_eq!(resp.status(), StatusCode::OK);
let uri = format!("{}/{}", DOCS.home, "favicon-32x32.png");
println!("{uri}");
let resp = test::call_service(&app, test::TestRequest::get().uri(&uri).to_request()).await;
assert_eq!(resp.status(), StatusCode::OK);
}
}

View file

@ -19,7 +19,6 @@ use std::env;
use actix_web::http::StatusCode;
use actix_web::web::JsonConfig;
use actix_web::{error::InternalError, middleware, App, HttpServer};
use actix_web_prom::PrometheusMetricsBuilder;
use clap::{Parser, Subcommand};
use log::info;
@ -27,7 +26,7 @@ use lazy_static::lazy_static;
mod api;
mod ctx;
mod docs;
//mod docs;
#[cfg(not(tarpaulin_include))]
mod errors;
//#[macro_use]
@ -113,11 +112,6 @@ async fn serve(settings: Settings, ctx: AppCtx) -> std::io::Result<()> {
let ip = settings.server.get_ip();
println!("Starting server on: http://{ip}");
let prometheus = PrometheusMetricsBuilder::new("api")
.endpoint("/metrics")
.build()
.unwrap();
HttpServer::new(move || {
App::new()
.wrap(middleware::Logger::default())
@ -130,7 +124,6 @@ async fn serve(settings: Settings, ctx: AppCtx) -> std::io::Result<()> {
middleware::TrailingSlash::Trim,
))
.app_data(get_json_err())
.wrap(prometheus.clone())
.configure(routes::services)
})
.bind(ip)?

View file

@ -17,6 +17,5 @@
use actix_web::web;
pub fn services(cfg: &mut web::ServiceConfig) {
crate::docs::services(cfg);
crate::api::v1::services(cfg);
}

View file

@ -17,17 +17,13 @@
use std::env;
use std::path::Path;
use config::{builder::DefaultState, Config, ConfigBuilder, ConfigError, Environment, File};
use config::{Config, ConfigError, Environment, File};
use derive_more::Display;
use log::info;
use log::warn;
use serde::Deserialize;
use serde::Serialize;
use url::Url;
const PREFIX: &str = "LPCONDUCTOR";
const SEPARATOR: &str = "_";
#[derive(Debug, Clone, Deserialize)]
pub struct Server {
pub port: u32,
@ -52,115 +48,58 @@ pub enum ConductorType {
Dummy,
}
#[derive(Debug, Clone, Deserialize)]
pub struct Creds {
pub token: String,
}
#[derive(Debug, Clone, Deserialize)]
pub struct Settings {
pub debug: bool,
pub creds: Creds,
pub server: Server,
pub source_code: Url,
pub source_code: String,
pub conductor: ConductorType,
}
#[cfg(not(tarpaulin_include))]
impl Settings {
pub fn authenticate(&self, token: &str) -> bool {
self.creds.token == token
}
pub fn new() -> Result<Self, ConfigError> {
let mut s = Config::builder();
let mut s = Config::new();
const CURRENT_DIR: &str = "./config/config.toml";
const ETC: &str = "/etc/librepages/conductor/config.toml";
const CURRENT_DIR: &str = "./config/default.toml";
const ETC: &str = "/etc/lpconductor/config.toml";
if let Ok(path) = env::var("LPCONDUCTOR_CONFIG") {
s = s.add_source(File::with_name(&path));
s.merge(File::with_name(&path))?;
} else if Path::new(CURRENT_DIR).exists() {
// merging default config from file
s = s.add_source(File::with_name(CURRENT_DIR));
s.merge(File::with_name(CURRENT_DIR))?;
} else if Path::new(ETC).exists() {
s = s.add_source(File::with_name(ETC));
s.merge(File::with_name(ETC))?;
} else {
warn!("configuration file not found");
log::warn!("configuration file not found");
}
s = s.add_source(Environment::with_prefix(PREFIX).separator(SEPARATOR));
s = set_separator_field(s);
s.merge(Environment::with_prefix("LPCONDUCTOR").separator("_"))?;
check_url(&s);
match env::var("PORT") {
Ok(val) => {
s = s.set_override("server.port", val).unwrap();
s.set("server.port", val).unwrap();
}
Err(e) => warn!("couldn't interpret PORT: {}", e),
}
let s = s.build()?;
match s.try_deserialize::<Self>() {
match s.try_into::<Self>() {
Ok(val) => {
Ok(val)
},
Err(e) => Err(ConfigError::Message(format!("\n\nError: {}. If it says missing fields, then please refer to https://git.batsense.net/LibrePages/conductor to learn more about how conductor reads configuration\n\n", e))),
Err(e) => Err(ConfigError::Message(format!("\n\nError: {}. If it says missing fields, then please refer to https://github.com/mCaptcha/mcaptcha#configuration to learn more about how mcaptcha reads configuration\n\n", e))),
}
}
}
#[cfg(not(tarpaulin_include))]
fn set_separator_field(mut s: ConfigBuilder<DefaultState>) -> ConfigBuilder<DefaultState> {
// ref: https://github.com/mehcode/config-rs/issues/391
fn check_url(s: &Config) {
let url = s
.get::<String>("source_code")
.expect("Couldn't access source_code");
fn from_env(
s: ConfigBuilder<DefaultState>,
env_name: &str,
config_name: &str,
) -> ConfigBuilder<DefaultState> {
if let Ok(val) = env::var(env_name) {
info!("Overriding {config_name} with data from env var {env_name}");
s.set_override(config_name, val)
.unwrap_or_else(|_| panic!("Couldn't set {config_name} from env var {env_name}"))
} else {
s
}
}
s = from_env(s, &format!("{PREFIX}{SEPARATOR}SOURCE_CODE"), "source_code");
s = from_env(
s,
&format!("{PREFIX}{SEPARATOR}SERVER{SEPARATOR}URL_PREFIX"),
"server.url_prefix",
);
s = from_env(
s,
&format!("{PREFIX}{SEPARATOR}SERVER{SEPARATOR}PROXY_HAS_TLS"),
"server.proxy_has_tls",
);
s = from_env(
s,
&format!("{PREFIX}{SEPARATOR}CREDS{SEPARATOR}TOKEN"),
"creds.token",
);
s
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn creds_works() {
let settings = Settings::new().unwrap();
let creds = settings.creds.clone();
assert!(settings.authenticate(&creds.token));
let mut creds = settings.creds.clone();
creds.token = "noexist".into();
assert!(!settings.authenticate(&creds.token))
}
Url::parse(&url).expect("Please enter a URL for source_code in settings");
}