Compare commits

...

11 Commits
v0.1.0 ... main

Author SHA1 Message Date
257120355a Update version 2023-08-27 14:37:22 +02:00
d5e41d6d5b Update README 2023-08-27 14:35:09 +02:00
a361f03409 Publish docker image on release 2023-08-27 14:24:10 +02:00
04778b5e63 Update dockerfile 2023-08-27 14:23:56 +02:00
4a0142f850 Add package details 2023-08-27 14:14:20 +02:00
14fe57183b Update dependencies 2023-08-27 14:09:46 +02:00
11153a5275 Expand usage of https only mode to github & fix typo 2022-05-04 17:35:35 +02:00
defda719f4 Add custom hosts and https only mode 2022-05-04 17:03:32 +02:00
1a29eefb70 Typo fixes 2022-02-25 12:21:21 +01:00
ByteDream
70ec489463
Changed slogan 2022-01-09 16:55:52 +01:00
1810105cb8 Fixed license link 2022-01-09 16:47:37 +01:00
8 changed files with 2069 additions and 132 deletions

1
.dockerignore Normal file
View File

@ -0,0 +1 @@
target/

View File

@ -1,5 +1,8 @@
HOST = 0.0.0.0 HOST=0.0.0.0
PORT = 8080 PORT=8080
ENABLE_REGEX = false ENABLE_REGEX=false
MAX_PATTER_LEN = 70 MAX_PATTER_LEN=70
HTTPS_ONLY=true
ENABLE_CUSTOM_HOSTS=false

36
.github/workflows/release.yml vendored Normal file
View File

@ -0,0 +1,36 @@
name: release
on:
push:
tags:
- v*
jobs:
docker-build-and-release:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Get version
run: echo "RELEASE_VERSION=$(echo ${{ github.ref_name }} | cut -c 2-)" >> $GITHUB_ENV
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v4
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: bytedream/smartrelease:${{ env.RELEASE_VERSION }},bytedream/smartrelease:latest

1760
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,17 +1,19 @@
[package] [package]
name = "smartrelease" name = "smartrelease"
version = "0.1.0" version = "0.1.1"
authors = ["ByteDream"]
edition = "2021" edition = "2021"
description = "Redirect to release assets dynamically"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html license = "MIT"
[dependencies] [dependencies]
actix-web = { version = "3.3", features = ["rustls"] } actix-web = { version = "4.3", features = ["rustls"] }
dotenv = "0.15" dotenv = "0.15"
env_logger = "0.9" env_logger = "0.10"
lazy_static = "1.4" lazy_static = "1.4"
log = "0.4" log = "0.4"
regex = "1.5" regex = "1.9"
reqwest = { version = "0.11", default-features = false, features = ["json", "rustls-tls"] }
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
@ -19,3 +21,4 @@ serde_json = "1.0"
lto = true lto = true
panic = "abort" panic = "abort"
opt-level = "z" opt-level = "z"
strip = true

View File

@ -1,4 +1,4 @@
FROM rust:1.57-alpine FROM rust:alpine as builder
WORKDIR /smartrelease WORKDIR /smartrelease
@ -8,8 +8,11 @@ RUN apk update && \
apk add musl-dev && \ apk add musl-dev && \
rm -rf /var/cache/apk rm -rf /var/cache/apk
RUN cargo build --release && \ RUN cargo build --release
ln -s target/release/smartrelease .
FROM alpine:latest
COPY --from=builder /smartrelease/target/release/smartrelease .
EXPOSE 8080 EXPOSE 8080

View File

@ -1,4 +1,4 @@
# smartrelease - Generate links to release assets dynamically # smartrelease - Redirect to release assets dynamically
[Releases](https://docs.github.com/en/repositories/releasing-projects-on-github/about-releases) are an essential feature of github (or other platforms like [gitea](https://gitea.io)) and the assets you can attach are pretty useful if you want to pre-compile a binary for example. [Releases](https://docs.github.com/en/repositories/releasing-projects-on-github/about-releases) are an essential feature of github (or other platforms like [gitea](https://gitea.io)) and the assets you can attach are pretty useful if you want to pre-compile a binary for example.
But linking to these assets directly in your README can be pretty annoying if every of your release asset has a version number in it, for example `program-v1.0.0`, and with every release the version number changes, and you have to change the direct link to it in your README. But linking to these assets directly in your README can be pretty annoying if every of your release asset has a version number in it, for example `program-v1.0.0`, and with every release the version number changes, and you have to change the direct link to it in your README.
@ -6,7 +6,7 @@ And this is where **smartrelease** enters the game.
It provides a simple yet powerful api endpoint which will redirect the user directly to the latest release asset you've specified in the api url. It provides a simple yet powerful api endpoint which will redirect the user directly to the latest release asset you've specified in the api url.
<p align="center"> <p align="center">
<a href="https://github.com/ByteDream/Yamete-Kudasai/releases/latest"> <a href="https://github.com/ByteDream/smartrelease/releases/latest">
<img src="https://img.shields.io/github/v/release/ByteDream/smartrelease?style=flat-square" alt="Latest release"> <img src="https://img.shields.io/github/v/release/ByteDream/smartrelease?style=flat-square" alt="Latest release">
</a> </a>
<a href="https://github.com/ByteDream/smartrelease/blob/master/LICENSE"> <a href="https://github.com/ByteDream/smartrelease/blob/master/LICENSE">
@ -80,7 +80,7 @@ These wildcards are currently supported:
In `smartrelease-v1.21.5-rc4`, `5` is the patch version number. In `smartrelease-v1.21.5-rc4`, `5` is the patch version number.
- `pre` - `pre`
_The pre-release number_. It can be a mix of numbers and letters (without any special character between). _The pre-release number_. It can be a mix of numbers and letters (without any special characters between).
In `smartrelease-v1.21.5-rc4`, `rc4` is the pre-release number. In `smartrelease-v1.21.5-rc4`, `rc4` is the pre-release number.
- `tag` - `tag`
@ -101,33 +101,33 @@ In case your asset name is not supported, but you want it to be supported, feel
For the example the [official instance](#official-instance) is used as host. For the example the [official instance](#official-instance) is used as host.
Latest release for this repo. Latest release for this repo.
The result looks like this: [Latest release](https://smartrelease.bytedream.org/github/ByteDream/smartrelease/smartrelease-v{major}.{minor}.{patch}_linux) The result looks like this: [Latest release](https://smartrelease.bytedream.dev/github/ByteDream/smartrelease/smartrelease-v{major}.{minor}.{patch}_linux)
``` ```
[Latest release](https://smartrelease.bytedream.org/github/ByteDream/smartrelease/smartrelease-v{major}.{minor}.{patch}_linux) [Latest release](https://smartrelease.bytedream.dev/github/ByteDream/smartrelease/smartrelease-v{major}.{minor}.{patch}_linux)
``` ```
We can also use [shields.io](https://shields.io) to make it look more appealing to the user. We can also use [shields.io](https://shields.io) to make it look more appealing to the user.
The result looks like this: [![Latest release](https://img.shields.io/github/v/release/ByteDream/smartrelease?style=flat-square)](https://smartrelease.bytedream.org/github/ByteDream/smartrelease/smartrelease-v{major}.{minor}.{patch}_linux) The result looks like this: [![Latest release](https://img.shields.io/github/v/release/ByteDream/smartrelease?style=flat-square)](https://smartrelease.bytedream.dev/github/ByteDream/smartrelease/smartrelease-v{major}.{minor}.{patch}_linux)
``` ```
[![Latest release](https://img.shields.io/github/v/release/ByteDream/smartrelease?style=flat-square)](https://smartrelease.bytedream.org/github/ByteDream/smartrelease/smartrelease-v{major}.{minor}.{patch}_linux) [![Latest release](https://img.shields.io/github/v/release/ByteDream/smartrelease?style=flat-square)](https://smartrelease.bytedream.dev/github/ByteDream/smartrelease/smartrelease-v{major}.{minor}.{patch}_linux)
``` ```
And now with the official Gitea instance (Gitea is a great open-source based alternative to GitHub, if you didn't knew it already) And now with the official Gitea instance (Gitea is a great open-source based alternative to GitHub, if you didn't knew it already)
The result looks like this: [Now with gitea!](https://smartrelease.bytedream.org/gitea/gitea/tea/tea-{major}.{minor}.{patch}-linux-amd64) The result looks like this: [Now with gitea!](https://smartrelease.bytedream.dev/gitea/gitea/tea/tea-{major}.{minor}.{patch}-linux-amd64)
``` ```
[Now with gitea!](https://smartrelease.bytedream.org/gitea/gitea/tea/tea-{major}.{minor}.{patch}-linux-amd64) [Now with gitea!](https://smartrelease.bytedream.dev/gitea/gitea/tea/tea-{major}.{minor}.{patch}-linux-amd64)
``` ```
## Hosting ## Hosting
## Official instance ## Official instance
The official instance is hosted on `https://smartrelease.bytedream.org`. The official instance is hosted on `https://smartrelease.bytedream.dev`.
It has regex disabled and a maximal pattern length of 70 character. It has regex disabled and a maximal pattern length of 70 characters.
So if you want, for example, using the official api for this repo, the following link will do it: So if you want, for example, using the official api for this repo, the following link will do it:
``` ```
https://smartrelease.bytedream.org/github/ByteDream/smartrelease/smartrelease-v{major}.{minor}.{patch}_linux https://smartrelease.bytedream.dev/github/ByteDream/smartrelease/smartrelease-v{major}.{minor}.{patch}_linux
``` ```
Nevertheless, I recommend you to host your own instance if you have the capabilities to do so since I cannot guarantee that my server will have a 100% uptime (but I will do my best to keep it online). Nevertheless, I recommend you to host your own instance if you have the capabilities to do so since I cannot guarantee that my server will have a 100% uptime (but I will do my best to keep it online).
@ -135,22 +135,20 @@ I also recommend you to visit this repo from time to time to see if something wi
## Self-hosting ## Self-hosting
_All following instructions are specified for linux, but at least [building](#build-it-from-source) should on every platform too_. _All following instructions are specified for linux, but at least [building](#build-it-from-source) should be possible on every platform_.
### Docker ### Docker
**Make sure you have [docker](https://docker.com) installed**. **Make sure you have [docker](https://docker.com) installed**.
Clone the repo via `git clone` or download the [zipfile](https://github.com/ByteDream/crunchyroll-go/archive/refs/heads/master.zip) and extract it. You can simply pull and run the docker image from [docker hub](https://hub.docker.com/r/bytedream/smartrelease).
Open a shell, enter the directory and follow the following commands:
```shell ```shell
[~/smartrelease]$ docker build -t smartrelease . [~]$ docker run --name smartrelease -p 8080:8080 bytedream/smartrelease:latest
[~/smartrelease]$ docker run -p 8080:8080 smartrelease
``` ```
### Binary ### Binary
Download the latest linux binary from [here](https://smartrelease.bytedream.org/github/ByteDream/smartrelease/smartrelease-v{major}.{minor}.{patch}_linux) (built with musl, so should work on libc and musl systems). Download the latest linux binary from [here](https://smartrelease.bytedream.dev/github/ByteDream/smartrelease/smartrelease-v{major}.{minor}.{patch}_linux) (built with musl, so should work on libc and musl systems).
Now simply execute binary and the server is up and running: Now simply execute binary and the server is up and running:
```shell ```shell
[~]$ ./smartrelease-v<version>_linux [~]$ ./smartrelease-v<version>_linux
@ -191,6 +189,20 @@ Default is `false`.
Limits the maximal length the pattern can have. Limits the maximal length the pattern can have.
Default is `70`. Default is `70`.
### `HTTPS_ONLY`
If requests should always be made with https connections. Default is `true`.
### `ENABLE_CUSTOM_HOSTS`
If custom hosted instances of git servers should be supported too. Default is `false`.
The url scheme of using a custom git server follows this:
<pre>
<code>https://example.com/custom/:host/<a href="#platform">:platform</a>/<a href="#owner">:owner</a>/<a href="#repository">:repository</a>/<a href="#pattern">:pattern</a></code>
</pre>
**:host** is the url of the custom git server host. Subpaths are currently not supported.
## Warnings ## Warnings
It is recommended to limit the pattern length with [`MAX_PATTER_LEN`](#max_patter_len) if [`ENABLE_REGEX`](#enable_regex) is enabled since a too long pattern which is too complex could lead to an, wanted or unwanted, [ReDoS](https://en.wikipedia.org/wiki/ReDoS) attack. It is recommended to limit the pattern length with [`MAX_PATTER_LEN`](#max_patter_len) if [`ENABLE_REGEX`](#enable_regex) is enabled since a too long pattern which is too complex could lead to an, wanted or unwanted, [ReDoS](https://en.wikipedia.org/wiki/ReDoS) attack.

View File

@ -1,27 +1,32 @@
use std::{env, io}; use actix_web::error::{ErrorBadRequest, ErrorInternalServerError, ErrorNotFound, ErrorUriTooLong};
use std::collections::HashMap; use actix_web::middleware::{ErrorHandlerResponse, ErrorHandlers};
use std::io::Write; use actix_web::{dev, get, http, web, App, Error, HttpRequest, HttpResponse, HttpServer, Result};
use std::str::FromStr;
use std::time::Duration;
use actix_web::{App, dev, Error, get, http, HttpRequest, HttpResponse, HttpServer, Result, web};
use actix_web::client::{Client, Connector};
use actix_web::error::{ErrorInternalServerError, ErrorNotFound, ErrorUriTooLong};
use actix_web::middleware::errhandlers::{ErrorHandlerResponse, ErrorHandlers};
use dotenv::dotenv; use dotenv::dotenv;
use lazy_static::lazy_static; use lazy_static::lazy_static;
use log::info; use log::info;
use log::LevelFilter::Info; use log::LevelFilter::Info;
use regex::{escape, Regex}; use regex::{escape, Regex};
use reqwest::Client;
use serde::Deserialize; use serde::Deserialize;
use std::collections::HashMap;
use std::io::Write;
use std::str::FromStr;
use std::{env, io};
lazy_static! { lazy_static! {
pub static ref ENABLE_REGEX: bool = env_lookup::<bool>("ENABLE_REGEX", false).unwrap(); static ref ENABLE_REGEX: bool = env_lookup("ENABLE_REGEX", false);
pub static ref MAX_PATTERN_LEN: i32 = env_lookup::<i32>("MAX_PATTER_LEN", 70).unwrap(); static ref MAX_PATTERN_LEN: i32 = env_lookup("MAX_PATTER_LEN", 70);
static ref HTTPS_ONLY: bool = env_lookup("HTTPS_ONLY", true);
pub static ref TAG_PATTERN: Regex = Regex::new(r"(?P<major>\d+)([.-_](?P<minor>\d+)([.-_](?P<patch>\d+))?([.-_]?(?P<pre>[\w\d]+))?)?").unwrap(); static ref ENABLE_CUSTOM_HOSTS: bool = env_lookup("ENABLE_CUSTOM_HOSTS", false);
pub static ref REPLACE_PATTERN: Regex = Regex::new(r"\{\w*?}").unwrap(); static ref TAG_PATTERN: Regex = Regex::new(
r"(?P<major>\d+)([.-_](?P<minor>\d+)([.-_](?P<patch>\d+))?([.-_]?(?P<pre>[\w\d]+))?)?"
pub static ref USER_AGENT: String = format!("smartrelease/{}", env_lookup::<String>("CARGO_PKG_VERSION", "".to_string()).unwrap()); )
.unwrap();
static ref REPLACE_PATTERN: Regex = Regex::new(r"\{\w*?}").unwrap();
static ref USER_AGENT: String = format!(
"smartrelease/{}",
env_lookup::<String>("CARGO_PKG_VERSION", "".to_string())
);
} }
#[derive(Deserialize)] #[derive(Deserialize)]
@ -38,138 +43,237 @@ struct Query {
minor: Option<String>, minor: Option<String>,
patch: Option<String>, patch: Option<String>,
pre: Option<String>, pre: Option<String>,
tag: Option<String> tag: Option<String>,
} }
#[derive(Deserialize)] #[derive(Deserialize)]
struct Assets { struct Assets {
name: String, name: String,
browser_download_url: String browser_download_url: String,
} }
#[derive(Deserialize)] #[derive(Deserialize)]
struct GitHub { struct GitHub {
tag_name: String, tag_name: String,
assets: Vec<Assets> assets: Vec<Assets>,
} }
#[get("/github/{user}/{repo}/{pattern}")] #[get("/github/{user}/{repo}/{pattern}")]
async fn github( async fn github(
web::Path((user, repo, pattern)): web::Path<(String, String, String)>, path: web::Path<(String, String, String)>,
query: web::Query<Query> query: web::Query<Query>,
) -> Result<HttpResponse> { ) -> Result<HttpResponse> {
if let Some(err) = pre_check(&pattern) { let (user, repo, pattern) = path.into_inner();
return Err(err)
}
let mut res = client() request_github(user.as_str(), repo.as_str(), pattern.as_str(), query).await
.get(format!("https://api.github.com/repos/{}/{}/releases/latest", user, repo))
.header("Accept", "application/vnd.github.v3+json")
.header("User-Agent", USER_AGENT.as_str())
.send()
.await?;
let mut github = res.json::<GitHub>().await?;
process(&pattern, &mut github.assets, query.into_inner(), &github.tag_name)
} }
#[derive(Deserialize)] #[derive(Deserialize)]
struct Gitea { struct Gitea {
tag_name: String, tag_name: String,
assets: Vec<Assets> assets: Vec<Assets>,
} }
#[get("/gitea/{user}/{repo}/{pattern}")] #[get("/gitea/{user}/{repo}/{pattern}")]
async fn gitea( async fn gitea(
web::Path((user, repo, pattern)): web::Path<(String, String, String)>, path: web::Path<(String, String, String)>,
query: web::Query<Query> query: web::Query<Query>,
) -> Result<HttpResponse> { ) -> Result<HttpResponse> {
let mut res = client() let (user, repo, pattern) = path.into_inner();
.get(format!("https://gitea.com/api/v1/repos/{}/{}/releases?limit=1", user, repo))
request_gitea(
"gitea.com",
user.as_str(),
repo.as_str(),
pattern.as_str(),
query,
)
.await
}
#[get("/custom/{host}/{platform}/{user}/{repo}/{pattern}")]
async fn custom(
path: web::Path<(String, String, String, String, String)>,
query: web::Query<Query>,
) -> Result<HttpResponse> {
let (host, platform, user, repo, pattern) = path.into_inner();
if *ENABLE_CUSTOM_HOSTS {
match platform.as_str() {
"gitea" => {
request_gitea(
host.as_str(),
user.as_str(),
repo.as_str(),
pattern.as_str(),
query,
)
.await
}
_ => Err(ErrorBadRequest("Invalid host")),
}
} else {
Err(ErrorNotFound("Custom hosts are disabled"))
}
}
async fn request_github(
user: &str,
repo: &str,
pattern: &str,
query: web::Query<Query>,
) -> Result<HttpResponse> {
if let Some(err) = pre_check(pattern) {
return Err(err);
}
let res = Client::new()
.get(format!(
"{}://api.github.com/repos/{}/{}/releases/latest",
if *HTTPS_ONLY { "https" } else { "http" },
user,
repo
))
.header("Accept", "application/vnd.github.v3+json")
.header("User-Agent", USER_AGENT.as_str())
.send()
.await
.map_err(ErrorInternalServerError)?;
let mut result = res
.json::<GitHub>()
.await
.map_err(ErrorInternalServerError)?;
process(
pattern,
&mut result.assets,
query.into_inner(),
&result.tag_name,
)
}
async fn request_gitea(
host: &str,
user: &str,
repo: &str,
pattern: &str,
query: web::Query<Query>,
) -> Result<HttpResponse> {
if let Some(err) = pre_check(pattern) {
return Err(err);
}
let res = Client::new()
.get(format!(
"{}://{}/api/v1/repos/{}/{}/releases?limit=1",
if *HTTPS_ONLY { "https" } else { "http" },
host,
user,
repo
))
.header(http::header::CONTENT_TYPE, "application/json") .header(http::header::CONTENT_TYPE, "application/json")
.header(http::header::USER_AGENT, USER_AGENT.as_str()) .header(http::header::USER_AGENT, USER_AGENT.as_str())
.send() .send()
.await?; .await
let mut gitea = res.json::<[Gitea; 1]>().await?; .map_err(ErrorInternalServerError)?;
let mut result = res
.json::<[Gitea; 1]>()
.await
.map_err(ErrorInternalServerError)?;
return process(&pattern, &mut gitea[0].assets, query.into_inner(), &gitea[0].tag_name) process(
pattern,
&mut result[0].assets,
query.into_inner(),
&result[0].tag_name,
)
} }
fn redirect_error<B>(res: dev::ServiceResponse<B>) -> Result<ErrorHandlerResponse<B>> { fn redirect_error<B>(res: dev::ServiceResponse<B>) -> Result<ErrorHandlerResponse<B>> {
if res.request().uri().path() == "/favicon" { if res.request().uri().path() == "/favicon" {
return Ok(ErrorHandlerResponse::Response(res)) return Ok(ErrorHandlerResponse::Response(res.map_into_left_body()));
} }
let split_path: Vec<&str> = res.request().uri().path().split("/").collect(); let split_path: Vec<&str> = res.request().uri().path().split('/').collect();
info!(
"{} {}: got {} ({})",
info!("{} {}: got {} ({})",
ip(res.request()), ip(res.request()),
res.request().path(), res.request().path(),
res.status().as_u16(), res.status().as_u16(),
res.response().error().map_or_else(|| String::new(), |v| v.to_string())); res.response()
.error()
.map_or_else(String::new, |v| v.to_string())
);
if split_path.len() >= 4 { if split_path.len() >= 4 {
let location = match *split_path.get(1).unwrap() { let location = match *split_path.get(1).unwrap() {
"github" => format!("https://github.com/{}/{}/releases/latest", "github" => format!(
*split_path.get(2).unwrap(), "https://github.com/{}/{}/releases/latest",
*split_path.get(3).unwrap()), *split_path.get(2).unwrap(),
"gitea" => format!("https://gitea.com/{}/{}/releases", *split_path.get(3).unwrap()
*split_path.get(2).unwrap(), ),
*split_path.get(3).unwrap()), "gitea" => format!(
_ => "".to_string() "https://gitea.com/{}/{}/releases",
*split_path.get(2).unwrap(),
*split_path.get(3).unwrap()
),
_ => "".to_string(),
}; };
if location != "" { if !location.is_empty() {
return Ok(ErrorHandlerResponse::Response( return Ok(ErrorHandlerResponse::Response(
res.into_response( res.into_response(
HttpResponse::Found() HttpResponse::Found()
.header(http::header::LOCATION, location) .insert_header((http::header::LOCATION, location))
.finish() .finish()
.into_body() .map_into_right_body(),
) ),
)) ));
} }
} }
Ok(ErrorHandlerResponse::Response(res)) Ok(ErrorHandlerResponse::Response(res.map_into_left_body()))
} }
fn client() -> Client { fn env_lookup<F: FromStr>(name: &str, default: F) -> F {
return Client::builder()
.timeout(Duration::from_secs(5))
.connector(
Connector::new()
.timeout(Duration::from_secs(3))
.finish()
)
.finish();
}
fn env_lookup<F: FromStr>(name: &str, default: F) -> std::result::Result<F, F::Err> {
if let Ok(envvar) = env::var(name) { if let Ok(envvar) = env::var(name) {
return envvar.parse::<F>(); envvar.parse::<F>().unwrap_or(default)
} else {
default
} }
Ok(default)
} }
fn ip(request: &HttpRequest) -> String { fn ip(request: &HttpRequest) -> String {
request request
.connection_info().realip_remote_addr().unwrap() .connection_info()
.rsplit_once(":").unwrap().0.to_string() .realip_remote_addr()
.unwrap()
.rsplit_once(':')
.unwrap()
.0
.to_string()
} }
fn pre_check(pattern: &String) -> Option<Error> { fn pre_check(pattern: &str) -> Option<Error> {
// if MAX_PATTERN_LEN is -1 or below the len checking is disabled // if MAX_PATTERN_LEN is -1 or below the len checking is disabled
if *MAX_PATTERN_LEN > -1 && REPLACE_PATTERN.replace_all(pattern.as_str(), "").len() > *MAX_PATTERN_LEN as usize { if *MAX_PATTERN_LEN > -1
return Some(ErrorUriTooLong(format!("Pattern / last url path must not exceed {} characters", *MAX_PATTERN_LEN))) && REPLACE_PATTERN.replace_all(pattern, "").len() > *MAX_PATTERN_LEN as usize
{
return Some(ErrorUriTooLong(format!(
"Pattern / last url path must not exceed {} characters",
*MAX_PATTERN_LEN
)));
} }
None None
} }
fn process(pattern: &String, assets: &mut Vec<Assets>, query: Query, tag_name: &String) -> Result<HttpResponse> { fn process(
let re: Regex; pattern: &str,
assets: &mut Vec<Assets>,
query: Query,
tag_name: &String,
) -> Result<HttpResponse> {
let mut replaced = replace( let mut replaced = replace(
pattern.to_string(), pattern.to_string(),
tag_name.to_string(), tag_name.to_string(),
@ -178,20 +282,23 @@ fn process(pattern: &String, assets: &mut Vec<Assets>, query: Query, tag_name: &
("minor", query.minor), ("minor", query.minor),
("patch", query.patch), ("patch", query.patch),
("pre", query.pre), ("pre", query.pre),
("tag", query.tag) ("tag", query.tag),
].iter().cloned().collect(), ]
query.clear_unknown.unwrap_or(true) .iter()
.cloned()
.collect(),
query.clear_unknown.unwrap_or(true),
); );
if !*ENABLE_REGEX { if !*ENABLE_REGEX {
replaced = escape(replaced.as_str()) replaced = escape(replaced.as_str())
} }
match Regex::new(replaced.as_str()) { let re = match Regex::new(replaced.as_str()) {
Ok(r) => re = r, Ok(r) => r,
Err(e) => { Err(e) => {
return Err(ErrorInternalServerError(e)); return Err(ErrorInternalServerError(e));
} }
} };
if query.reverse.unwrap_or(false) { if query.reverse.unwrap_or(false) {
assets.reverse() assets.reverse()
@ -199,14 +306,24 @@ fn process(pattern: &String, assets: &mut Vec<Assets>, query: Query, tag_name: &
for asset in assets { for asset in assets {
if re.is_match(asset.name.as_str()) { if re.is_match(asset.name.as_str()) {
return Ok(HttpResponse::Found().set_header(http::header::LOCATION, format!("{}", asset.browser_download_url)).finish()) return Ok(HttpResponse::Found()
.insert_header((
http::header::LOCATION,
asset.browser_download_url.to_string(),
))
.finish());
} }
} }
Err(ErrorNotFound("No matching asset was found")) Err(ErrorNotFound("No matching asset was found"))
} }
fn replace(pattern: String, tag: String, alternatives: HashMap<&str, Option<String>>, clear_unknown: bool) -> String { fn replace(
pattern: String,
tag: String,
alternatives: HashMap<&str, Option<String>>,
clear_unknown: bool,
) -> String {
let mut result = pattern; let mut result = pattern;
if let Some(regex_match) = TAG_PATTERN.captures(tag.as_str()) { if let Some(regex_match) = TAG_PATTERN.captures(tag.as_str()) {
@ -253,29 +370,31 @@ async fn main() -> io::Result<()> {
.filter_level(Info) .filter_level(Info)
.init(); .init();
let host = env_lookup::<String>("HOST", "0.0.0.0".to_string()).unwrap(); let host = env_lookup::<String>("HOST", "0.0.0.0".to_string());
let port = env_lookup::<i16>("PORT", 8080).unwrap(); let port = env_lookup::<i16>("PORT", 8080);
let server = HttpServer::new(|| { let server = HttpServer::new(|| {
App::new() App::new()
.service(github) .service(github)
.service(gitea) .service(gitea)
.service( .service(custom)
web::resource("/").route(web::get().to(|| async { .service(web::resource("/").route(web::get().to(|| async {
HttpResponse::Found() HttpResponse::Found()
.header(http::header::LOCATION, "https://github.com/ByteDream/smartrelease") .insert_header((
.finish() http::header::LOCATION,
}) "https://github.com/ByteDream/smartrelease",
)) ))
.finish()
})))
.wrap( .wrap(
ErrorHandlers::new() ErrorHandlers::new()
.handler(http::StatusCode::BAD_REQUEST, redirect_error) .handler(http::StatusCode::BAD_REQUEST, redirect_error)
.handler(http::StatusCode::NOT_FOUND, redirect_error) .handler(http::StatusCode::NOT_FOUND, redirect_error)
.handler(http::StatusCode::GATEWAY_TIMEOUT, redirect_error) .handler(http::StatusCode::GATEWAY_TIMEOUT, redirect_error),
) )
}) })
.bind(format!("{}:{}", host, port))? .bind(format!("{}:{}", host, port))?
.run(); .run();
info!( info!(
"Started server on {}:{} with regex {} and a max pattern len of {}", "Started server on {}:{} with regex {} and a max pattern len of {}",