mirror of
https://github.com/orbien-org/orbien.git
synced 2026-09-21 16:01:31 +00:00
Merge branch 'release/v3.6.1'
# Conflicts: # Cargo.lock # Cargo.toml # README.md # README_ZH.md # client-java/orbien-client/pom.xml # client-java/orbien-client/src/main/java/io/github/lxien/orbien/client/OrbienClient.java # client-java/orbien-spring-boot-demo/pom.xml # client-java/orbien-spring-boot-starter/pom.xml # client-java/pom.xml # docs/docs/integrations/springboot.mdx # docs/i18n/en/docusaurus-plugin-content-docs/current/integrations/springboot.mdx # docs/package.json # docs/src/components/DownloadMatrix/index.tsx # server-ui/package-lock.json # server-ui/package.json
This commit is contained in:
Generated
+4
-4
@@ -3939,7 +3939,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "orbien-client"
|
||||
version = "3.6.0"
|
||||
version = "3.6.1-beta.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -3960,7 +3960,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "orbien-core"
|
||||
version = "3.6.0"
|
||||
version = "3.6.1-beta.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64",
|
||||
@@ -3991,7 +3991,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "orbien-desktop"
|
||||
version = "3.6.0"
|
||||
version = "3.6.1-beta.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"libc",
|
||||
@@ -4012,7 +4012,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "orbien-server"
|
||||
version = "3.6.0"
|
||||
version = "3.6.1-beta.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ members = [
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "3.6.0"
|
||||
version = "3.6.1-beta.1"
|
||||
edition = "2021"
|
||||
license = "Apache-2.0"
|
||||
authors = ["orbien"]
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<img src="https://img.shields.io/badge/Rust-Tokio-orange?style=for-the-badge&logo=rust&logoColor=white" alt="Rust"/>
|
||||
</a>
|
||||
<a href="https://github.com/orbien-org/orbien/releases">
|
||||
<img src="https://img.shields.io/badge/orbien-3.6.0-blue?style=for-the-badge" alt="orbien:3.6.0"/>
|
||||
<img src="https://img.shields.io/badge/orbien-3.6.1--beta.1-blue?style=for-the-badge" alt="orbien:3.6.1-beta.1"/>
|
||||
</a>
|
||||
<a href="https://somsubhra.github.io/github-release-stats/?username=orbien-org&repository=orbien">
|
||||
<img src="https://img.shields.io/github/downloads/orbien-org/orbien/total?style=for-the-badge" alt="Downloads"/>
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@
|
||||
<img src="https://img.shields.io/badge/Rust-Tokio-orange?style=for-the-badge&logo=rust&logoColor=white" alt="Rust"/>
|
||||
</a>
|
||||
<a href="https://github.com/orbien-org/orbien/releases">
|
||||
<img src="https://img.shields.io/badge/orbien-3.6.0-blue?style=for-the-badge" alt="orbien:3.6.0"/>
|
||||
<img src="https://img.shields.io/badge/orbien-3.6.1--beta.1-blue?style=for-the-badge" alt="orbien:3.6.1-beta.1"/>
|
||||
</a>
|
||||
<a href="https://somsubhra.github.io/github-release-stats/?username=orbien-org&repository=orbien">
|
||||
<img src="https://img.shields.io/github/downloads/orbien-org/orbien/total?style=for-the-badge" alt="Downloads"/>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<parent>
|
||||
<groupId>io.github.lxien</groupId>
|
||||
<artifactId>orbien</artifactId>
|
||||
<version>3.6.0</version>
|
||||
<version>3.6.1-beta.1</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>orbien-client</artifactId>
|
||||
|
||||
+9
-31
@@ -20,7 +20,6 @@ import io.netty.channel.socket.SocketChannel;
|
||||
import io.netty.channel.socket.nio.NioSocketChannel;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.nio.file.Path;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
@@ -31,7 +30,7 @@ import org.slf4j.LoggerFactory;
|
||||
|
||||
public final class OrbienClient implements AutoCloseable {
|
||||
private static final Logger log = LoggerFactory.getLogger(OrbienClient.class);
|
||||
private static final String VERSION = "3.6.0";
|
||||
private static final String VERSION = "3.6.1-beta.1";
|
||||
|
||||
private final OrbienClientConfig config;
|
||||
private final AtomicBoolean started = new AtomicBoolean(false);
|
||||
@@ -59,11 +58,12 @@ public final class OrbienClient implements AutoCloseable {
|
||||
started.set(false);
|
||||
throw new IllegalStateException("tcpMux is not supported; set tcpMux=false on client and server");
|
||||
}
|
||||
if (config.getToken() == null || config.getToken().isEmpty()) {
|
||||
log.warn("auth.token is empty; authentication is disabled");
|
||||
}
|
||||
|
||||
group = new NioEventLoopGroup();
|
||||
CompletableFuture<String> loginFuture = new CompletableFuture<>();
|
||||
Path sessionIdPath = resolveSessionIdPath();
|
||||
String previousSessionId = resolvePreviousSessionId(sessionIdPath);
|
||||
|
||||
try {
|
||||
Bootstrap b = new Bootstrap();
|
||||
@@ -91,11 +91,9 @@ public final class OrbienClient implements AutoCloseable {
|
||||
ChannelFuture cf =
|
||||
b.connect(connectHost(config.getServerHost()), config.getServerPort()).sync();
|
||||
controlChannel = cf.channel();
|
||||
sendLogin(controlChannel, previousSessionId);
|
||||
sendLogin(controlChannel);
|
||||
|
||||
String id = loginFuture.get(30, TimeUnit.SECONDS);
|
||||
config.setSessionId(id);
|
||||
SessionIdStore.save(sessionIdPath, id);
|
||||
log.info("connected to {} sessionId={}", config.getServer(), id);
|
||||
} catch (Exception e) {
|
||||
close();
|
||||
@@ -103,26 +101,7 @@ public final class OrbienClient implements AutoCloseable {
|
||||
}
|
||||
}
|
||||
|
||||
private Path resolveSessionIdPath() {
|
||||
String configured = config.getSessionIdFile();
|
||||
if (configured != null && !configured.isBlank()) {
|
||||
return Path.of(configured);
|
||||
}
|
||||
return SessionIdStore.defaultPath();
|
||||
}
|
||||
|
||||
private String resolvePreviousSessionId(Path sessionIdPath) {
|
||||
if (config.getSessionId() != null && !config.getSessionId().isBlank()) {
|
||||
return config.getSessionId().trim();
|
||||
}
|
||||
String loaded = SessionIdStore.load(sessionIdPath);
|
||||
if (!loaded.isEmpty()) {
|
||||
log.info("restored sessionId={} from {}", loaded, sessionIdPath);
|
||||
}
|
||||
return loaded;
|
||||
}
|
||||
|
||||
private void sendLogin(Channel ch, String previousSessionId) {
|
||||
private void sendLogin(Channel ch) {
|
||||
long ts = System.currentTimeMillis() / 1000;
|
||||
Login login = new Login();
|
||||
login.version = VERSION;
|
||||
@@ -132,15 +111,14 @@ public final class OrbienClient implements AutoCloseable {
|
||||
login.user = config.getUser();
|
||||
login.timestamp = ts;
|
||||
login.authDigest = AuthKeys.computeAuthDigest(config.getToken(), ts);
|
||||
login.sessionId = previousSessionId == null ? "" : previousSessionId;
|
||||
login.sessionId = "";
|
||||
login.poolCount = Math.max(config.getPoolCount(), 1);
|
||||
ch.writeAndFlush(new WireMessage(MsgType.LOGIN, login));
|
||||
log.debug(
|
||||
"login sent hostname={} user={} poolCount={} sessionId={}",
|
||||
"login sent hostname={} user={} poolCount={} sessionId=<new>",
|
||||
login.hostname,
|
||||
login.user,
|
||||
login.poolCount,
|
||||
login.sessionId.isEmpty() ? "<new>" : login.sessionId);
|
||||
login.poolCount);
|
||||
}
|
||||
|
||||
private void openDataConn(String currentSessionId) {
|
||||
|
||||
-18
@@ -14,8 +14,6 @@ public final class OrbienClientConfig {
|
||||
private boolean tcpMux = false;
|
||||
private int poolCount = 1;
|
||||
private String user = "";
|
||||
private String sessionId = "";
|
||||
private String sessionIdFile = "";
|
||||
private int heartbeatIntervalSecs = 30;
|
||||
private final List<TunnelConfig> tunnels = new ArrayList<>();
|
||||
|
||||
@@ -70,22 +68,6 @@ public final class OrbienClientConfig {
|
||||
this.user = user == null ? "" : user;
|
||||
}
|
||||
|
||||
public String getSessionId() {
|
||||
return sessionId;
|
||||
}
|
||||
|
||||
public void setSessionId(String sessionId) {
|
||||
this.sessionId = sessionId == null ? "" : sessionId;
|
||||
}
|
||||
|
||||
public String getSessionIdFile() {
|
||||
return sessionIdFile;
|
||||
}
|
||||
|
||||
public void setSessionIdFile(String sessionIdFile) {
|
||||
this.sessionIdFile = sessionIdFile == null ? "" : sessionIdFile;
|
||||
}
|
||||
|
||||
public int getHeartbeatIntervalSecs() {
|
||||
return heartbeatIntervalSecs;
|
||||
}
|
||||
|
||||
-59
@@ -1,59 +0,0 @@
|
||||
package io.github.lxien.orbien.client;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
final class SessionIdStore {
|
||||
private static final Logger log = LoggerFactory.getLogger(SessionIdStore.class);
|
||||
|
||||
static final String DEFAULT_FILE = ".orbien.session_id";
|
||||
private SessionIdStore() {}
|
||||
|
||||
static Path defaultPath() {
|
||||
return Path.of(DEFAULT_FILE);
|
||||
}
|
||||
|
||||
static String load(Path path) {
|
||||
if (path == null || !Files.isRegularFile(path)) {
|
||||
return "";
|
||||
}
|
||||
try {
|
||||
String s = Files.readString(path, StandardCharsets.UTF_8).trim();
|
||||
if (s.isEmpty() || s.length() > 64) {
|
||||
return "";
|
||||
}
|
||||
for (int i = 0; i < s.length(); i++) {
|
||||
char c = s.charAt(i);
|
||||
if (!(c >= '0' && c <= '9'
|
||||
|| c >= 'a' && c <= 'f'
|
||||
|| c >= 'A' && c <= 'F'
|
||||
|| c == '-')) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
return s;
|
||||
} catch (IOException e) {
|
||||
log.warn("failed to load sessionId from {}: {}", path, e.toString());
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
static void save(Path path, String sessionId) {
|
||||
if (path == null || sessionId == null || sessionId.isBlank()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Path parent = path.getParent();
|
||||
if (parent != null) {
|
||||
Files.createDirectories(parent);
|
||||
}
|
||||
Files.writeString(path, sessionId, StandardCharsets.UTF_8);
|
||||
} catch (IOException e) {
|
||||
log.warn("failed to persist sessionId to {}: {}", path, e.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
<parent>
|
||||
<groupId>io.github.lxien</groupId>
|
||||
<artifactId>orbien</artifactId>
|
||||
<version>3.6.0</version>
|
||||
<version>3.6.1-beta.1</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>orbien-spring-boot-demo</artifactId>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<parent>
|
||||
<groupId>io.github.lxien</groupId>
|
||||
<artifactId>orbien</artifactId>
|
||||
<version>3.6.0</version>
|
||||
<version>3.6.1-beta.1</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>orbien-spring-boot-starter</artifactId>
|
||||
|
||||
-20
@@ -21,8 +21,6 @@ public class OrbienProperties {
|
||||
private int poolCount = 1;
|
||||
private String user = "";
|
||||
private int heartbeatIntervalSecs = 30;
|
||||
private String sessionId = "";
|
||||
private String sessionIdFile = "";
|
||||
|
||||
@NestedConfigurationProperty
|
||||
private final Tunnel tunnel = new Tunnel();
|
||||
@@ -83,22 +81,6 @@ public class OrbienProperties {
|
||||
this.heartbeatIntervalSecs = heartbeatIntervalSecs;
|
||||
}
|
||||
|
||||
public String getSessionId() {
|
||||
return sessionId;
|
||||
}
|
||||
|
||||
public void setSessionId(String sessionId) {
|
||||
this.sessionId = sessionId == null ? "" : sessionId;
|
||||
}
|
||||
|
||||
public String getSessionIdFile() {
|
||||
return sessionIdFile;
|
||||
}
|
||||
|
||||
public void setSessionIdFile(String sessionIdFile) {
|
||||
this.sessionIdFile = sessionIdFile == null ? "" : sessionIdFile;
|
||||
}
|
||||
|
||||
public Tunnel getTunnel() {
|
||||
return tunnel;
|
||||
}
|
||||
@@ -118,8 +100,6 @@ public class OrbienProperties {
|
||||
cfg.setPoolCount(poolCount);
|
||||
cfg.setUser(user);
|
||||
cfg.setHeartbeatIntervalSecs(heartbeatIntervalSecs);
|
||||
cfg.setSessionId(sessionId);
|
||||
cfg.setSessionIdFile(sessionIdFile);
|
||||
if (hasTunnel()) {
|
||||
OrbienClientConfig.TunnelConfig p = new OrbienClientConfig.TunnelConfig();
|
||||
String name = tunnel.getName();
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@
|
||||
|
||||
<groupId>io.github.lxien</groupId>
|
||||
<artifactId>orbien</artifactId>
|
||||
<version>3.6.0</version>
|
||||
<version>3.6.1-beta.1</version>
|
||||
<packaging>pom</packaging>
|
||||
|
||||
<name>orbien</name>
|
||||
|
||||
@@ -32,6 +32,9 @@ pub struct Service {
|
||||
|
||||
impl Service {
|
||||
pub fn new(cfg: ClientConfig) -> Self {
|
||||
if cfg.auth.token.is_empty() {
|
||||
tracing::warn!("auth.token is empty; authentication is disabled");
|
||||
}
|
||||
Self { cfg }
|
||||
}
|
||||
|
||||
|
||||
+38
-1
@@ -1,3 +1,40 @@
|
||||
mod replay;
|
||||
mod token;
|
||||
|
||||
pub use token::{compute_auth_digest, verify_auth_digest, verify_login};
|
||||
pub use replay::ReplayCache;
|
||||
pub use token::{compute_auth_digest, unix_now_secs, verify_auth_digest, verify_login};
|
||||
|
||||
use std::fmt;
|
||||
|
||||
pub const AUTH_SKEW_SECS: i64 = 180;
|
||||
|
||||
pub const REPLAY_TTL_SECS: u64 = (AUTH_SKEW_SECS as u64) * 2;
|
||||
|
||||
pub const REPLAY_MAX_ENTRIES: usize = 100_000;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum AuthFailure {
|
||||
EmptyDigest,
|
||||
InvalidDigest,
|
||||
TimestampSkew,
|
||||
Replay,
|
||||
Capacity,
|
||||
}
|
||||
|
||||
impl AuthFailure {
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::EmptyDigest => "empty authentication digest",
|
||||
Self::InvalidDigest => "invalid authentication digest",
|
||||
Self::TimestampSkew => "timestamp outside allowed window",
|
||||
Self::Replay => "authentication digest reused",
|
||||
Self::Capacity => "authentication replay cache full",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for AuthFailure {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
use super::{AuthFailure, REPLAY_MAX_ENTRIES, REPLAY_TTL_SECS};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Mutex;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct ReplayCache {
|
||||
inner: Mutex<HashMap<String, Instant>>,
|
||||
}
|
||||
|
||||
impl ReplayCache {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn accept(&self, digest: &str) -> Result<(), AuthFailure> {
|
||||
self.accept_at(digest, Instant::now())
|
||||
}
|
||||
|
||||
pub fn accept_at(&self, digest: &str, now: Instant) -> Result<(), AuthFailure> {
|
||||
let mut map = self.inner.lock().unwrap_or_else(|e| e.into_inner());
|
||||
purge_expired(&mut map, now);
|
||||
|
||||
if map.contains_key(digest) {
|
||||
return Err(AuthFailure::Replay);
|
||||
}
|
||||
if map.len() >= REPLAY_MAX_ENTRIES {
|
||||
return Err(AuthFailure::Capacity);
|
||||
}
|
||||
|
||||
map.insert(
|
||||
digest.to_owned(),
|
||||
now + Duration::from_secs(REPLAY_TTL_SECS),
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn purge_expired(map: &mut HashMap<String, Instant>, now: Instant) {
|
||||
map.retain(|_, exp| *exp > now);
|
||||
}
|
||||
+41
-14
@@ -1,3 +1,4 @@
|
||||
use super::{AuthFailure, ReplayCache, AUTH_SKEW_SECS};
|
||||
use hmac::{Hmac, Mac};
|
||||
use sha2::Sha256;
|
||||
|
||||
@@ -10,23 +11,49 @@ pub fn compute_auth_digest(token: &str, timestamp: i64) -> String {
|
||||
hex::encode(mac.finalize().into_bytes())
|
||||
}
|
||||
|
||||
pub fn verify_login(token: &str, auth_digest: &str, timestamp: i64) -> bool {
|
||||
verify_auth_digest(token, auth_digest, timestamp)
|
||||
}
|
||||
|
||||
pub fn verify_auth_digest(token: &str, auth_digest: &str, timestamp: i64) -> bool {
|
||||
pub fn verify_auth_digest(
|
||||
token: &str,
|
||||
auth_digest: &str,
|
||||
timestamp: i64,
|
||||
now_secs: i64,
|
||||
replay: Option<&ReplayCache>,
|
||||
) -> Result<(), AuthFailure> {
|
||||
if token.is_empty() {
|
||||
return true;
|
||||
return Ok(());
|
||||
}
|
||||
if auth_digest.is_empty() {
|
||||
return false;
|
||||
return Err(AuthFailure::EmptyDigest);
|
||||
}
|
||||
let Ok(expected) = hex::decode(auth_digest) else {
|
||||
return false;
|
||||
};
|
||||
let Ok(mut mac) = HmacSha256::new_from_slice(token.as_bytes()) else {
|
||||
return false;
|
||||
};
|
||||
if (now_secs - timestamp).abs() > AUTH_SKEW_SECS {
|
||||
return Err(AuthFailure::TimestampSkew);
|
||||
}
|
||||
|
||||
let expected = hex::decode(auth_digest).map_err(|_| AuthFailure::InvalidDigest)?;
|
||||
let mut mac =
|
||||
HmacSha256::new_from_slice(token.as_bytes()).map_err(|_| AuthFailure::InvalidDigest)?;
|
||||
mac.update(timestamp.to_string().as_bytes());
|
||||
mac.verify_slice(&expected).is_ok()
|
||||
mac.verify_slice(&expected)
|
||||
.map_err(|_| AuthFailure::InvalidDigest)?;
|
||||
|
||||
if let Some(cache) = replay {
|
||||
cache.accept(auth_digest)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn verify_login(
|
||||
token: &str,
|
||||
auth_digest: &str,
|
||||
timestamp: i64,
|
||||
now_secs: i64,
|
||||
replay: &ReplayCache,
|
||||
) -> Result<(), AuthFailure> {
|
||||
verify_auth_digest(token, auth_digest, timestamp, now_secs, Some(replay))
|
||||
}
|
||||
|
||||
pub fn unix_now_secs() -> i64 {
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_secs() as i64)
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
@@ -150,7 +150,7 @@ where
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut TaskContext<'_>,
|
||||
) -> Poll<Result<(), std::io::Error>> {
|
||||
match Pin::new(&mut self.get_mut().inner).poll_flush(cx) {
|
||||
match Pin::new(&mut self.get_mut().inner).poll_close(cx) {
|
||||
Poll::Ready(Ok(())) => Poll::Ready(Ok(())),
|
||||
Poll::Ready(Err(e)) => Poll::Ready(Err(std::io::Error::other(e))),
|
||||
Poll::Pending => Poll::Pending,
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
use super::stream::{boxed_stream, DynStream};
|
||||
use anyhow::{anyhow, Result};
|
||||
use std::future::poll_fn;
|
||||
use std::time::Duration;
|
||||
use tokio::sync::{mpsc, oneshot};
|
||||
use tokio_util::compat::{FuturesAsyncReadCompatExt, TokioAsyncReadCompatExt};
|
||||
|
||||
const MAX_NUM_STREAMS: usize = 4096;
|
||||
const CLOSE_TIMEOUT: Duration = Duration::from_secs(1);
|
||||
|
||||
fn yamux_config() -> yamux::Config {
|
||||
let mut cfg = yamux::Config::default();
|
||||
@@ -55,7 +57,12 @@ async fn drive_client(io: DynStream, mut open_rx: mpsc::Receiver<OpenReply>) {
|
||||
let _ = reply.send(res);
|
||||
}
|
||||
None => {
|
||||
let _ = poll_fn(|cx| conn.poll_close(cx)).await;
|
||||
if tokio::time::timeout(CLOSE_TIMEOUT, poll_fn(|cx| conn.poll_close(cx)))
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
tracing::debug!("yamux client close timed out, dropping connection");
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,34 +19,32 @@ import TabItem from '@theme/TabItem';
|
||||
<dependency>
|
||||
<groupId>io.github.lxien</groupId>
|
||||
<artifactId>orbien-spring-boot-starter</artifactId>
|
||||
<version>3.6.0</version>
|
||||
<version>3.6.1-beta.1</version>
|
||||
</dependency>
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem value="Gradle" label="Gradle">
|
||||
```groovy
|
||||
implementation("io.github.lxien:orbien-spring-boot-starter:3.6.0")
|
||||
implementation("io.github.lxien:orbien-spring-boot-starter:3.6.1-beta.1")
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## 最小配置
|
||||
|
||||
`local-port` / `name` 可省略:未填时分别使用 Spring Boot Web 端口与 `spring.application.name`(或 `orbien-{protocol}`)。
|
||||
|
||||
```yaml
|
||||
# application.yml
|
||||
orbien:
|
||||
enabled: true
|
||||
server: "127.0.0.1:9527"
|
||||
# token: YOUR_TOKEN # 可选
|
||||
# token: YOUR_TOKEN
|
||||
tunnel:
|
||||
protocol: http
|
||||
domains:
|
||||
- web.domain.com
|
||||
- web.example.com
|
||||
```
|
||||
|
||||
服务端需关闭多路复用,例如:
|
||||
服务端需关闭多路复用:
|
||||
|
||||
```toml
|
||||
# orbien-server.toml
|
||||
@@ -54,7 +52,20 @@ listen = "0.0.0.0:9527"
|
||||
httpGwPort = 80
|
||||
|
||||
[transport]
|
||||
tcpMux = false # 关闭 TCP 多路复用
|
||||
tcpMux = false
|
||||
```
|
||||
|
||||
## TCP 示例
|
||||
|
||||
```yaml
|
||||
orbien:
|
||||
enabled: true
|
||||
server: "127.0.0.1:9527"
|
||||
token: YOUR_TOKEN
|
||||
tunnel:
|
||||
protocol: tcp
|
||||
# local-port: 3306
|
||||
remote-port: 9000
|
||||
```
|
||||
|
||||
## 完整参考
|
||||
@@ -65,18 +76,35 @@ orbien:
|
||||
enabled: true
|
||||
server: "127.0.0.1:9527"
|
||||
token: YOUR_TOKEN
|
||||
tcp-mux: false # 必须为 false,且与服务端一致
|
||||
pool-count: 1 # 登录时预申请的数据连接池大小
|
||||
tcp-mux: false # 必须为 false
|
||||
pool-count: 1
|
||||
heartbeat-interval-secs: 30
|
||||
user: spring-boot-demo # 可选,展示在 Dashboard
|
||||
# session-id: "" # 可选;为空则从 session-id-file 恢复,或自动生成
|
||||
# session-id-file: "" # 可选;为空则使用用户目录下的默认路径
|
||||
user: spring-boot-demo
|
||||
tunnel:
|
||||
name: web-demo # 可选
|
||||
protocol: http # tcp | http
|
||||
name: web-demo
|
||||
protocol: http
|
||||
local-ip: 127.0.0.1
|
||||
# local-port: 8080 # 可选;未填则使用 Spring Boot 监听端口
|
||||
# remote-port: 9000 # 仅 protocol=tcp 时需要
|
||||
domains: # protocol=http 时必填
|
||||
- web.domain.com
|
||||
# local-port: 8080
|
||||
# remote-port: 9000
|
||||
domains:
|
||||
- web.example.com
|
||||
# - admin
|
||||
```
|
||||
|
||||
## 配置项说明
|
||||
|
||||
| 配置项 | 默认值 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `orbien.enabled` | `true` | 是否启用 |
|
||||
| `orbien.server` | `127.0.0.1:9527` | 服务端地址|
|
||||
| `orbien.token` | 空 | 鉴权 token|
|
||||
| `orbien.tcp-mux` | `false` | 仅允许 `false`|
|
||||
| `orbien.pool-count` | `1` | 登录时预申请的数据连接数|
|
||||
| `orbien.heartbeat-interval-secs` | `30` | 控制面心跳间隔(秒)|
|
||||
| `orbien.user` | 空 | 展示用用户名 |
|
||||
| `orbien.tunnel.name` | 见上文 | 隧道名 |
|
||||
| `orbien.tunnel.protocol` | `tcp` | `tcp` 或 `http` |
|
||||
| `orbien.tunnel.local-ip` | `127.0.0.1` | 本地服务 IP |
|
||||
| `orbien.tunnel.local-port` | `0` | 本地服务端口|
|
||||
| `orbien.tunnel.remote-port` | `0` | 服务端对外端口;`protocol=tcp` 时必填 |
|
||||
| `orbien.tunnel.domains` | `[]` | HTTP 域名列表;`protocol=http` 时必填 |
|
||||
|
||||
@@ -11,7 +11,7 @@ import TabItem from '@theme/TabItem';
|
||||
|
||||
- Tunnel protocols: **TCP** and **HTTP** only
|
||||
- Transport: **TCP** only
|
||||
- **No TCP multiplexing**: set `orbien.tcp-mux` to `false`, and set `[transport].tcpMux = false` on the server (multiplexing is on by default; turn it off when using the Java client)
|
||||
- **No TCP multiplexing**: `orbien.tcp-mux` must be `false`, and the server must also set `[transport].tcpMux = false` (multiplexing is on by default; turn it off when using the Java client)
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="Maven" label="Maven" default>
|
||||
@@ -19,34 +19,32 @@ import TabItem from '@theme/TabItem';
|
||||
<dependency>
|
||||
<groupId>io.github.lxien</groupId>
|
||||
<artifactId>orbien-spring-boot-starter</artifactId>
|
||||
<version>3.6.0</version>
|
||||
<version>3.6.1-beta.1</version>
|
||||
</dependency>
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem value="Gradle" label="Gradle">
|
||||
```groovy
|
||||
implementation("io.github.lxien:orbien-spring-boot-starter:3.6.0")
|
||||
implementation("io.github.lxien:orbien-spring-boot-starter:3.6.1-beta.1")
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Minimal configuration
|
||||
|
||||
`local-port` and `name` are optional. When omitted, the starter uses the Spring Boot web port and `spring.application.name` (or `orbien-{protocol}`).
|
||||
|
||||
```yaml
|
||||
# application.yml
|
||||
orbien:
|
||||
enabled: true
|
||||
server: "127.0.0.1:9527"
|
||||
# token: YOUR_TOKEN # optional
|
||||
# token: YOUR_TOKEN
|
||||
tunnel:
|
||||
protocol: http
|
||||
domains:
|
||||
- web.domain.com
|
||||
- web.example.com
|
||||
```
|
||||
|
||||
Disable multiplexing on the server, for example:
|
||||
Disable multiplexing on the server:
|
||||
|
||||
```toml
|
||||
# orbien-server.toml
|
||||
@@ -54,7 +52,20 @@ listen = "0.0.0.0:9527"
|
||||
httpGwPort = 80
|
||||
|
||||
[transport]
|
||||
tcpMux = false # disable TCP multiplexing
|
||||
tcpMux = false
|
||||
```
|
||||
|
||||
## TCP example
|
||||
|
||||
```yaml
|
||||
orbien:
|
||||
enabled: true
|
||||
server: "127.0.0.1:9527"
|
||||
token: YOUR_TOKEN
|
||||
tunnel:
|
||||
protocol: tcp
|
||||
# local-port: 3306
|
||||
remote-port: 9000
|
||||
```
|
||||
|
||||
## Full reference
|
||||
@@ -65,18 +76,35 @@ orbien:
|
||||
enabled: true
|
||||
server: "127.0.0.1:9527"
|
||||
token: YOUR_TOKEN
|
||||
tcp-mux: false # must be false and match the server
|
||||
pool-count: 1 # data-connection pool size requested at login
|
||||
tcp-mux: false # must be false
|
||||
pool-count: 1
|
||||
heartbeat-interval-secs: 30
|
||||
user: spring-boot-demo # optional; shown on the dashboard
|
||||
# session-id: "" # optional; if empty, restore from session-id-file or generate
|
||||
# session-id-file: "" # optional; if empty, use the default path under the user home
|
||||
user: spring-boot-demo
|
||||
tunnel:
|
||||
name: web-demo # optional
|
||||
protocol: http # tcp | http
|
||||
name: web-demo
|
||||
protocol: http
|
||||
local-ip: 127.0.0.1
|
||||
# local-port: 8080 # optional; defaults to the Spring Boot listen port
|
||||
# remote-port: 9000 # required when protocol=tcp
|
||||
domains: # required when protocol=http
|
||||
- web.domain.com
|
||||
# local-port: 8080
|
||||
# remote-port: 9000
|
||||
domains:
|
||||
- web.example.com
|
||||
# - admin
|
||||
```
|
||||
|
||||
## Configuration reference
|
||||
|
||||
| Key | Default | Description |
|
||||
| --- | --- | --- |
|
||||
| `orbien.enabled` | `true` | Whether to enable |
|
||||
| `orbien.server` | `127.0.0.1:9527` | Server address |
|
||||
| `orbien.token` | empty | Auth token |
|
||||
| `orbien.tcp-mux` | `false` | Only `false` is allowed |
|
||||
| `orbien.pool-count` | `1` | Data connections pre-allocated at login |
|
||||
| `orbien.heartbeat-interval-secs` | `30` | Control-plane heartbeat interval (seconds) |
|
||||
| `orbien.user` | empty | Display username |
|
||||
| `orbien.tunnel.name` | see above | Tunnel name |
|
||||
| `orbien.tunnel.protocol` | `tcp` | `tcp` or `http` |
|
||||
| `orbien.tunnel.local-ip` | `127.0.0.1` | Local service IP |
|
||||
| `orbien.tunnel.local-port` | `0` | Local service port |
|
||||
| `orbien.tunnel.remote-port` | `0` | Public port on the server; required when `protocol=tcp` |
|
||||
| `orbien.tunnel.domains` | `[]` | HTTP domain list; required when `protocol=http` |
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "orbien-docs",
|
||||
"version": "3.6.0",
|
||||
"version": "3.6.1-beta.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"docusaurus": "docusaurus",
|
||||
|
||||
@@ -7,7 +7,7 @@ import styles from './styles.module.css';
|
||||
|
||||
const REPO = 'orbien-org/orbien';
|
||||
|
||||
const FALLBACK_VERSION = '3.6.0';
|
||||
const FALLBACK_VERSION = '3.6.1-beta.1';
|
||||
|
||||
type OsId = 'windows' | 'linux' | 'darwin' | 'freebsd';
|
||||
type ArchId = 'amd64' | 'arm64';
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "orbien-server-ui",
|
||||
"version": "3.6.0",
|
||||
"version": "3.6.1-beta.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "orbien-server-ui",
|
||||
"version": "3.6.0",
|
||||
"version": "3.6.1-beta.1",
|
||||
"dependencies": {
|
||||
"vue": "^3.5.22",
|
||||
"vue-i18n": "^11.4.8",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "orbien-server-ui",
|
||||
"private": true,
|
||||
"version": "3.6.0",
|
||||
"version": "3.6.1-beta.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -21,6 +21,10 @@ use tokio::time::sleep;
|
||||
type CtrlRead = ReadHalf<DynStream>;
|
||||
type CtrlWrite = WriteHalf<DynStream>;
|
||||
|
||||
const KICK_WRITE_TIMEOUT: Duration = Duration::from_secs(1);
|
||||
const WRITER_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(2);
|
||||
const FINISHED_WAIT_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
|
||||
pub struct Control {
|
||||
pub session_id: String,
|
||||
pub generation: u64,
|
||||
@@ -253,8 +257,16 @@ impl Control {
|
||||
break;
|
||||
}
|
||||
self.reap_bg_tasks().await;
|
||||
if self.closed.load(Ordering::SeqCst) {
|
||||
break;
|
||||
}
|
||||
let shutdown = self.shutdown_notify.notified();
|
||||
tokio::pin!(shutdown);
|
||||
if self.closed.load(Ordering::SeqCst) {
|
||||
break;
|
||||
}
|
||||
let msg = tokio::select! {
|
||||
_ = self.shutdown_notify.notified() => {
|
||||
_ = &mut shutdown => {
|
||||
break;
|
||||
}
|
||||
msg = async {
|
||||
@@ -311,7 +323,7 @@ impl Control {
|
||||
pub async fn shutdown(&self) {
|
||||
self.signal_close();
|
||||
if self.cleaning.swap(true, Ordering::SeqCst) {
|
||||
self.wait_finished().await;
|
||||
let _ = tokio::time::timeout(FINISHED_WAIT_TIMEOUT, self.wait_finished()).await;
|
||||
return;
|
||||
}
|
||||
{
|
||||
@@ -322,8 +334,20 @@ impl Control {
|
||||
}
|
||||
}
|
||||
{
|
||||
let mut writer = self.writer.lock().await;
|
||||
let _ = writer.shutdown().await;
|
||||
let shut = async {
|
||||
let mut writer = self.writer.lock().await;
|
||||
let _ = writer.shutdown().await;
|
||||
};
|
||||
if tokio::time::timeout(WRITER_SHUTDOWN_TIMEOUT, shut)
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
tracing::warn!(
|
||||
session_id = %self.session_id,
|
||||
generation = self.generation,
|
||||
"control writer shutdown timed out"
|
||||
);
|
||||
}
|
||||
}
|
||||
{
|
||||
let mut bg = self.bg_tasks.lock().await;
|
||||
@@ -335,15 +359,24 @@ impl Control {
|
||||
|
||||
pub async fn kick(&self, reason: impl Into<String>) {
|
||||
let reason = reason.into();
|
||||
{
|
||||
self.signal_close();
|
||||
let wrote = tokio::time::timeout(KICK_WRITE_TIMEOUT, async {
|
||||
let mut writer = self.writer.lock().await;
|
||||
let _ = msg::write_msg(
|
||||
msg::write_msg(
|
||||
&mut *writer,
|
||||
&Message::KickOut(KickOut {
|
||||
reason: reason.clone(),
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
.await
|
||||
})
|
||||
.await;
|
||||
if !matches!(wrote, Ok(Ok(()))) {
|
||||
tracing::debug!(
|
||||
session_id = %self.session_id,
|
||||
generation = self.generation,
|
||||
"kick-out write skipped or failed"
|
||||
);
|
||||
}
|
||||
tracing::info!(
|
||||
session_id = %self.session_id,
|
||||
|
||||
@@ -9,6 +9,7 @@ use crate::tunnel::{
|
||||
};
|
||||
use agent_registry::AgentRegistry;
|
||||
use anyhow::{anyhow, Result};
|
||||
use orbien_core::auth::ReplayCache;
|
||||
use orbien_core::config::ServerConfig;
|
||||
use orbien_core::transport;
|
||||
use session_table::SessionMap;
|
||||
@@ -24,6 +25,7 @@ pub struct Service {
|
||||
cfg: ServerConfig,
|
||||
pub(crate) controls: Arc<Mutex<SessionMap>>,
|
||||
pub(crate) agents: Arc<AgentRegistry>,
|
||||
pub(crate) auth_replay: Arc<ReplayCache>,
|
||||
http_gw: Option<Arc<HttpGw>>,
|
||||
https_gw: Option<Arc<HttpsGw>>,
|
||||
tls_config: Arc<rustls::ServerConfig>,
|
||||
@@ -36,6 +38,9 @@ pub struct Service {
|
||||
|
||||
impl Service {
|
||||
pub fn new(cfg: ServerConfig) -> Result<Self> {
|
||||
if cfg.auth.token.is_empty() {
|
||||
tracing::warn!("auth.token is empty; authentication is disabled");
|
||||
}
|
||||
let http_gw = if cfg.http_gw_enabled() {
|
||||
Some(Arc::new(HttpGw::new(cfg.http_gw_port)))
|
||||
} else {
|
||||
@@ -56,6 +61,7 @@ impl Service {
|
||||
cfg,
|
||||
controls: Arc::new(Mutex::new(HashMap::new())),
|
||||
agents: Arc::new(AgentRegistry::new()),
|
||||
auth_replay: Arc::new(ReplayCache::new()),
|
||||
http_gw,
|
||||
https_gw,
|
||||
tls_config,
|
||||
@@ -191,7 +197,6 @@ impl Service {
|
||||
let tunnel_count = control.tunnel_count().await;
|
||||
let generation = control.generation;
|
||||
control.kick("kicked from dashboard").await;
|
||||
control.wait_finished().await;
|
||||
|
||||
{
|
||||
let mut map = self.controls.lock().await;
|
||||
|
||||
@@ -22,7 +22,14 @@ impl Service {
|
||||
login: Login,
|
||||
peer: SocketAddr,
|
||||
) -> Result<()> {
|
||||
if !auth::verify_login(&self.cfg.auth.token, &login.auth_digest, login.timestamp) {
|
||||
if let Err(reason) = auth::verify_login(
|
||||
&self.cfg.auth.token,
|
||||
&login.auth_digest,
|
||||
login.timestamp,
|
||||
auth::unix_now_secs(),
|
||||
&self.auth_replay,
|
||||
) {
|
||||
tracing::warn!(%reason, %peer, "login rejected");
|
||||
let mut stream = stream;
|
||||
let _ = msg::write_msg(
|
||||
&mut stream,
|
||||
@@ -98,7 +105,6 @@ impl Service {
|
||||
"replacing prior control session"
|
||||
);
|
||||
old.shutdown().await;
|
||||
old.wait_finished().await;
|
||||
}
|
||||
|
||||
match self.agents.try_online(AgentOnlineSpec {
|
||||
@@ -149,12 +155,13 @@ impl Service {
|
||||
let metrics = Arc::clone(&self.metrics);
|
||||
let rid = session_id.clone();
|
||||
let result = Arc::clone(&control).run().await;
|
||||
control.shutdown().await;
|
||||
metrics.close_client();
|
||||
|
||||
let tunnel_count = control.tunnel_count().await;
|
||||
let _ = remove_if_current(&controls, &rid, &control).await;
|
||||
agents.release(&rid, generation, tunnel_count);
|
||||
control.shutdown().await;
|
||||
if remove_if_current(&controls, &rid, &control).await {
|
||||
agents.release(&rid, generation, tunnel_count);
|
||||
}
|
||||
metrics.close_client();
|
||||
|
||||
result
|
||||
}
|
||||
@@ -167,9 +174,20 @@ impl Service {
|
||||
if nw.session_id.trim().is_empty() {
|
||||
return Err(anyhow!("empty session_id for data conn"));
|
||||
}
|
||||
if !auth::verify_auth_digest(&self.cfg.auth.token, &nw.auth_digest, nw.timestamp) {
|
||||
if let Err(reason) = auth::verify_auth_digest(
|
||||
&self.cfg.auth.token,
|
||||
&nw.auth_digest,
|
||||
nw.timestamp,
|
||||
auth::unix_now_secs(),
|
||||
None,
|
||||
) {
|
||||
tracing::warn!(
|
||||
%reason,
|
||||
session_id = %nw.session_id,
|
||||
"data connection authentication failed"
|
||||
);
|
||||
return Err(anyhow!(
|
||||
"data conn auth failed for session_id={}",
|
||||
"data connection authentication failed for session_id={}",
|
||||
nw.session_id
|
||||
));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user