mirror of
https://github.com/ivabus/binhost
synced 2024-11-23 16:25:07 +03:00
Initial commit
Signed-off-by: Ivan Bushchik <ivabus@ivabus.dev>
This commit is contained in:
commit
514718f05b
8 changed files with 415 additions and 0 deletions
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
/target
|
||||||
|
bin
|
||||||
|
*.DS_Store
|
||||||
|
.idea
|
11
.rustfmt.toml
Normal file
11
.rustfmt.toml
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
edition = "2021"
|
||||||
|
hard_tabs = true
|
||||||
|
merge_derives = true
|
||||||
|
reorder_imports = true
|
||||||
|
reorder_modules = true
|
||||||
|
use_field_init_shorthand = true
|
||||||
|
use_small_heuristics = "Off"
|
||||||
|
|
||||||
|
# May need nightly rustfmt
|
||||||
|
wrap_comments = true
|
||||||
|
comment_width = 80
|
13
Cargo.toml
Normal file
13
Cargo.toml
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
[package]
|
||||||
|
name = "binhost"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
clap = { version = "4.4.11", features = ["derive"] }
|
||||||
|
rocket = "0.5.0"
|
||||||
|
serde = { version = "1.0.193", features = ["derive"] }
|
||||||
|
once_cell = "1.19.0"
|
||||||
|
sha2 = "0.10.8"
|
20
LICENSE
Normal file
20
LICENSE
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2023 Ivan Bushchik
|
||||||
|
|
||||||
|
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.
|
84
README.md
Normal file
84
README.md
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
# BinHost
|
||||||
|
|
||||||
|
> HTTP server to easily serve (prebuilt) binaries for any (UNIX-like) platform
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```shell
|
||||||
|
cargo install --git https://github.com/ivabus/binhost
|
||||||
|
```
|
||||||
|
|
||||||
|
## Server usage
|
||||||
|
|
||||||
|
List options with `--help`
|
||||||
|
|
||||||
|
#### Directory structure
|
||||||
|
|
||||||
|
Directory, passed to `binhost` `--dir` option (defaults to `./bin`) should look like (for `hello` binary)
|
||||||
|
|
||||||
|
Note: list of binaries will be refreshed every 5 minutes (by default, see `--refresh` option)
|
||||||
|
|
||||||
|
```tree
|
||||||
|
bin
|
||||||
|
└── hello
|
||||||
|
├── Darwin
|
||||||
|
│ ├── arm64
|
||||||
|
│ │ └── hello
|
||||||
|
│ └── x86_64
|
||||||
|
│ └── hello
|
||||||
|
└── Linux
|
||||||
|
└── aarch64
|
||||||
|
└── hello
|
||||||
|
```
|
||||||
|
|
||||||
|
## Client usage
|
||||||
|
|
||||||
|
### Get available binaries
|
||||||
|
|
||||||
|
#### Request
|
||||||
|
|
||||||
|
```http request
|
||||||
|
GET / HTTP/1.1
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Example response
|
||||||
|
|
||||||
|
```
|
||||||
|
- hello (platforms: ["Linux-aarch64", "Darwin-arm64", "Darwin-x86_64"])
|
||||||
|
```
|
||||||
|
|
||||||
|
### Get script for specific binary (suitable for `curl | sh` syntax)
|
||||||
|
|
||||||
|
This script will determine platform and arch and download necessary binary (and check hashsum if `sha256sum` binary is present in `$PATH`).
|
||||||
|
|
||||||
|
#### Request
|
||||||
|
|
||||||
|
```http request
|
||||||
|
GET /<BIN> HTTP/1.1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Get binary for specific platform
|
||||||
|
|
||||||
|
#### Request
|
||||||
|
|
||||||
|
```http request
|
||||||
|
GET /bin/<BIN>/<PLATFORM>/<ARCH> HTTP/1.1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Get sha256 hash of binary for specific platform
|
||||||
|
|
||||||
|
#### Request
|
||||||
|
|
||||||
|
```http request
|
||||||
|
GET /bin/<BIN>/<PLATFORM>/<ARCH>/sha256 HTTP/1.1
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Example response
|
||||||
|
|
||||||
|
```text
|
||||||
|
a5d1fba1c28b60038fb1008a3c482b4119070a537af86a05046dedbe8f85e18d hello
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This project is licensed under [MIT License](./LICENSE)
|
175
src/main.rs
Normal file
175
src/main.rs
Normal file
|
@ -0,0 +1,175 @@
|
||||||
|
mod structs;
|
||||||
|
use structs::*;
|
||||||
|
|
||||||
|
use clap::Parser;
|
||||||
|
use rocket::figment::Figment;
|
||||||
|
use rocket::fs::NamedFile;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
|
#[macro_use]
|
||||||
|
extern crate rocket;
|
||||||
|
|
||||||
|
use rocket::http::Status;
|
||||||
|
use rocket::response::content::RawText;
|
||||||
|
use rocket::tokio::io::AsyncReadExt;
|
||||||
|
use sha2::digest::FixedOutput;
|
||||||
|
use sha2::Digest;
|
||||||
|
|
||||||
|
static mut BINS: Option<(HashMap<String, Bin>, Instant)> = None;
|
||||||
|
|
||||||
|
static WEB_SH: &str = include_str!("../web.sh");
|
||||||
|
|
||||||
|
async fn reload_bins(bins: (&mut HashMap<String, Bin>, &mut Instant), args: &Args) {
|
||||||
|
if (Instant::now() - *bins.1).as_secs() > args.refresh {
|
||||||
|
*bins.0 = get_bins(args).await;
|
||||||
|
*bins.1 = Instant::now();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_bins(args: &Args) -> HashMap<String, Bin> {
|
||||||
|
let mut bins: HashMap<String, Bin> = HashMap::new();
|
||||||
|
std::fs::read_dir(&args.dir).unwrap().for_each(|entry| {
|
||||||
|
let en = entry.unwrap();
|
||||||
|
if en.path().is_dir() {
|
||||||
|
let mut bin: Bin = Bin {
|
||||||
|
name: en.file_name().into_string().unwrap(),
|
||||||
|
platforms: vec![],
|
||||||
|
};
|
||||||
|
std::fs::read_dir(en.path()).unwrap().for_each(|platform| {
|
||||||
|
let plat = platform.unwrap();
|
||||||
|
std::fs::read_dir(plat.path()).unwrap().for_each(|arch| {
|
||||||
|
let ar = arch.unwrap();
|
||||||
|
bin.platforms.push(Platform {
|
||||||
|
system: plat.file_name().into_string().unwrap(),
|
||||||
|
arch: ar.file_name().into_string().unwrap(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
bins.insert(bin.name.clone(), bin);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
bins
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_platform_list(bin: &Bin) -> String {
|
||||||
|
let mut s = String::new();
|
||||||
|
for i in &bin.platforms {
|
||||||
|
s.push_str(&format!("{}-{}|", i.system, i.arch))
|
||||||
|
}
|
||||||
|
s.pop().unwrap();
|
||||||
|
s
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/")]
|
||||||
|
async fn index() -> RawText<String> {
|
||||||
|
let args = Args::parse();
|
||||||
|
let mut ret = String::new();
|
||||||
|
unsafe {
|
||||||
|
if let Some((bins, time)) = &mut BINS {
|
||||||
|
reload_bins((bins, time), &args).await;
|
||||||
|
if bins.is_empty() {
|
||||||
|
return RawText(String::from("No binaries found"));
|
||||||
|
}
|
||||||
|
for (name, bin) in bins {
|
||||||
|
ret.push_str(&format!(
|
||||||
|
"- {} (platforms: {:?})\n",
|
||||||
|
name,
|
||||||
|
bin.platforms
|
||||||
|
.iter()
|
||||||
|
.map(|plat| format!("{}-{}", plat.system, plat.arch))
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RawText(ret)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/<bin>")]
|
||||||
|
async fn get_script(bin: &str) -> ScriptResponse {
|
||||||
|
let args = Args::parse();
|
||||||
|
unsafe {
|
||||||
|
if let Some((bins, time)) = &mut BINS {
|
||||||
|
reload_bins((bins, time), &args).await;
|
||||||
|
return match bins.get(bin) {
|
||||||
|
None => ScriptResponse::Status(Status::NotFound),
|
||||||
|
Some(bin) => {
|
||||||
|
let mut script = String::from(WEB_SH);
|
||||||
|
script = script
|
||||||
|
.replace("{{NAME}}", &bin.name)
|
||||||
|
.replace("{{PLATFORM_LIST}}", &format_platform_list(bin))
|
||||||
|
.replace("{{EXTERNAL_ADDRESS}}", &args.url);
|
||||||
|
ScriptResponse::Text(RawText(script))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ScriptResponse::Status(Status::NotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/bin/<bin>/<platform>/<arch>")]
|
||||||
|
async fn get_binary(bin: &str, platform: &str, arch: &str) -> BinaryResponse {
|
||||||
|
let args = Args::parse();
|
||||||
|
let file = NamedFile::open(format!(
|
||||||
|
"{}/{}/{}/{}/{}",
|
||||||
|
args.dir.file_name().unwrap().to_str().unwrap(),
|
||||||
|
bin,
|
||||||
|
platform,
|
||||||
|
arch,
|
||||||
|
bin
|
||||||
|
))
|
||||||
|
.await;
|
||||||
|
match file {
|
||||||
|
Ok(f) => BinaryResponse::Bin(f),
|
||||||
|
Err(_) => BinaryResponse::Status(Status::BadRequest),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/bin/<bin>/<platform>/<arch>/sha256")]
|
||||||
|
async fn get_binary_hash(bin: &str, platform: &str, arch: &str) -> ScriptResponse {
|
||||||
|
let args = Args::parse();
|
||||||
|
let file = NamedFile::open(format!(
|
||||||
|
"{}/{}/{}/{}/{}",
|
||||||
|
args.dir.file_name().unwrap().to_str().unwrap(),
|
||||||
|
bin,
|
||||||
|
platform,
|
||||||
|
arch,
|
||||||
|
bin
|
||||||
|
))
|
||||||
|
.await;
|
||||||
|
|
||||||
|
match file {
|
||||||
|
Ok(mut f) => {
|
||||||
|
let mut hasher = sha2::Sha256::new();
|
||||||
|
let mut contents: Vec<u8> = vec![];
|
||||||
|
f.read_to_end(&mut contents).await.unwrap();
|
||||||
|
hasher.update(contents);
|
||||||
|
ScriptResponse::Text(RawText(format!(
|
||||||
|
"{:x} {}",
|
||||||
|
hasher.finalize_fixed(),
|
||||||
|
f.path().file_name().unwrap().to_str().unwrap()
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
Err(_) => ScriptResponse::Status(Status::BadRequest),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[launch]
|
||||||
|
async fn rocket() -> _ {
|
||||||
|
let args = Args::parse();
|
||||||
|
if !args.dir.exists() {
|
||||||
|
eprintln!("Directory with binary files does not exist");
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
BINS = Some((get_bins(&args).await, Instant::now()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let figment = Figment::from(rocket::Config::default())
|
||||||
|
.merge(("ident", "binhost"))
|
||||||
|
.merge(("port", args.port))
|
||||||
|
.merge(("address", args.address));
|
||||||
|
rocket::custom(figment).mount("/", routes![index, get_script, get_binary, get_binary_hash])
|
||||||
|
}
|
54
src/structs.rs
Normal file
54
src/structs.rs
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
use clap::Parser;
|
||||||
|
use rocket::fs::NamedFile;
|
||||||
|
use rocket::http::Status;
|
||||||
|
use rocket::response::content::RawText;
|
||||||
|
use std::net::IpAddr;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
#[derive(Parser, Clone, Debug)]
|
||||||
|
#[command(author, version)]
|
||||||
|
pub struct Args {
|
||||||
|
/// Directory where binary files are contained
|
||||||
|
#[arg(long, default_value = "bin")]
|
||||||
|
pub dir: PathBuf,
|
||||||
|
|
||||||
|
/// Refresh time (in secs)
|
||||||
|
#[arg(long, default_value = "300")]
|
||||||
|
pub refresh: u64,
|
||||||
|
|
||||||
|
/// External address (url)
|
||||||
|
#[arg(long, default_value = "http://127.0.0.1:8000")]
|
||||||
|
pub url: String,
|
||||||
|
|
||||||
|
/// Address to listen on
|
||||||
|
#[arg(short, long, default_value = "127.0.0.1")]
|
||||||
|
pub address: IpAddr,
|
||||||
|
|
||||||
|
/// Port to listen on
|
||||||
|
#[arg(short, long, default_value = "8000")]
|
||||||
|
pub port: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Platform {
|
||||||
|
pub system: String,
|
||||||
|
pub arch: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Bin {
|
||||||
|
pub name: String,
|
||||||
|
pub platforms: Vec<Platform>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Responder)]
|
||||||
|
pub enum ScriptResponse {
|
||||||
|
Status(Status),
|
||||||
|
Text(RawText<String>),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Responder)]
|
||||||
|
pub enum BinaryResponse {
|
||||||
|
Status(Status),
|
||||||
|
Bin(NamedFile),
|
||||||
|
}
|
54
web.sh
Normal file
54
web.sh
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
#!/bin/sh
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
set -e -o pipefail
|
||||||
|
|
||||||
|
NAME="{{NAME}}"
|
||||||
|
|
||||||
|
if ! uname > /dev/null; then
|
||||||
|
echo "No \`uname\` command was found, cannot continue" 1>&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! expr "{{PLATFORM_LIST}}" : "\(.*$(uname)-$(uname -m).*\)" > /dev/null; then
|
||||||
|
echo Platform $(uname)-$(uname -m) is not supported 1>&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
DOWNLOAD_COMMAND="curl"
|
||||||
|
OUTPUT_ARG="-o"
|
||||||
|
DIR="/tmp/binhost-$NAME-$(date +%s)"
|
||||||
|
FILE="$DIR/$NAME"
|
||||||
|
|
||||||
|
if ! which curl > /dev/null; then
|
||||||
|
if ! which wget > /dev/null; then
|
||||||
|
echo "No curl or wget found, install one and rerun the script" 1>&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
export DOWNLOAD_COMMAND="wget"
|
||||||
|
export OUTPUT_ARG="-O"
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir $DIR
|
||||||
|
|
||||||
|
echo ":: Downloading binary" 1>&2
|
||||||
|
|
||||||
|
# shellcheck disable=SC1083
|
||||||
|
$DOWNLOAD_COMMAND {{EXTERNAL_ADDRESS}}/bin/$NAME/$(uname)/$(uname -m) $OUTPUT_ARG "$FILE"
|
||||||
|
|
||||||
|
chmod +x "$FILE"
|
||||||
|
|
||||||
|
cd $DIR
|
||||||
|
|
||||||
|
if ! which sha256sum > /dev/null; then
|
||||||
|
echo "No \`sha256sum\` command found, continuing without checking" 1>&2
|
||||||
|
else
|
||||||
|
echo ":: Checking hashsum" 1>&2
|
||||||
|
if ! ($DOWNLOAD_COMMAND {{EXTERNAL_ADDRESS}}/bin/$NAME/$(uname)/$(uname -m)/sha256 $OUTPUT_ARG - | sha256sum -c - > /dev/null); then
|
||||||
|
echo "sha256 is invalid" 1>&2
|
||||||
|
exit 255
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
$FILE < /dev/tty
|
||||||
|
|
||||||
|
rm "$FILE"
|
Loading…
Reference in a new issue