Skip to content
体验新版
项目
组织
正在加载...
登录
切换导航
打开侧边栏
后端镜像
Tabby
提交
0971a85d
T
Tabby
项目概览
后端镜像
/
Tabby
通知
31
Star
0
Fork
0
代码
文件
提交
分支
Tags
贡献者
分支图
Diff
Issue
0
列表
看板
标记
里程碑
合并请求
0
DevOps
流水线
流水线任务
计划
Wiki
0
Wiki
分析
仓库
DevOps
项目成员
Pages
T
Tabby
项目概览
项目概览
详情
发布
仓库
仓库
文件
提交
分支
标签
贡献者
分支图
比较
Issue
0
Issue
0
列表
看板
标记
里程碑
合并请求
0
合并请求
0
Pages
DevOps
DevOps
流水线
流水线任务
计划
分析
分析
仓库分析
DevOps
Wiki
0
Wiki
成员
成员
收起侧边栏
关闭侧边栏
动态
分支图
创建新Issue
流水线任务
提交
Issue看板
体验新版 GitCode,发现更多精彩内容 >>
未验证
提交
0971a85d
编写于
12月 13, 2020
作者:
E
Eugene Pankov
浏览文件
操作
浏览文件
下载
电子邮件补丁
差异文件
ssh: added dynamic port forwarding (fixes #2077)
上级
51d54a84
变更
6
隐藏空白更改
内联
并排
Showing
6 changed file
with
180 addition
and
25 deletion
+180
-25
terminus-ssh/package.json
terminus-ssh/package.json
+3
-1
terminus-ssh/src/api.ts
terminus-ssh/src/api.ts
+51
-19
terminus-ssh/src/components/sshPortForwardingModal.component.pug
...s-ssh/src/components/sshPortForwardingModal.component.pug
+19
-2
terminus-ssh/src/services/ssh.service.ts
terminus-ssh/src/services/ssh.service.ts
+2
-1
terminus-ssh/webpack.config.js
terminus-ssh/webpack.config.js
+1
-0
terminus-ssh/yarn.lock
terminus-ssh/yarn.lock
+104
-2
未找到文件。
terminus-ssh/package.json
浏览文件 @
0971a85d
...
...
@@ -26,9 +26,11 @@
"ansi-colors"
:
"^4.1.1"
,
"cli-spinner"
:
"^0.2.10"
,
"run-script-os"
:
"^1.1.3"
,
"ssh2"
:
"^0.8.2"
,
"socksv5"
:
"^0.0.6"
,
"ssh2"
:
"^0.8.9"
,
"ssh2-streams"
:
"Eugeny/ssh2-streams#75f6d3425d071ac73a18fd46e2f5e738bfe897c5"
,
"sshpk"
:
"^1.16.1"
,
"strip-ansi"
:
"^6.0.0"
,
"temp"
:
"^0.9.1"
,
"terminus-terminal"
:
"^1.0.98-nightly.0"
},
...
...
terminus-ssh/src/api.ts
浏览文件 @
0971a85d
import
colors
from
'
ansi-colors
'
import
stripAnsi
from
'
strip-ansi
'
import
socksv5
from
'
socksv5
'
import
{
BaseSession
}
from
'
terminus-terminal
'
import
{
Server
,
Socket
,
createServer
,
createConnection
}
from
'
net
'
import
{
Client
,
ClientChannel
}
from
'
ssh2
'
...
...
@@ -43,7 +45,7 @@ export interface SSHConnection {
}
export
enum
PortForwardType
{
Local
,
Remote
Local
,
Remote
,
Dynamic
}
export
class
ForwardedPort
{
...
...
@@ -55,13 +57,40 @@ export class ForwardedPort {
private
listener
:
Server
async
startLocalListener
(
callback
:
(
Socket
)
=>
void
):
Promise
<
void
>
{
this
.
listener
=
createServer
(
callback
)
return
new
Promise
((
resolve
,
reject
)
=>
{
this
.
listener
.
listen
(
this
.
port
,
'
127.0.0.1
'
)
this
.
listener
.
on
(
'
error
'
,
reject
)
this
.
listener
.
on
(
'
listening
'
,
resolve
)
})
async
startLocalListener
(
callback
:
(
accept
:
()
=>
Socket
,
reject
:
()
=>
void
,
sourceAddress
:
string
|
null
,
sourcePort
:
number
|
null
,
targetAddress
:
string
,
targetPort
:
number
)
=>
void
):
Promise
<
void
>
{
if
(
this
.
type
===
PortForwardType
.
Local
)
{
this
.
listener
=
createServer
(
s
=>
callback
(
()
=>
s
,
()
=>
s
.
destroy
(),
s
.
remoteAddress
??
null
,
s
.
remotePort
??
null
,
this
.
targetAddress
,
this
.
targetPort
,
))
return
new
Promise
((
resolve
,
reject
)
=>
{
this
.
listener
.
listen
(
this
.
port
,
this
.
host
)
this
.
listener
.
on
(
'
error
'
,
reject
)
this
.
listener
.
on
(
'
listening
'
,
resolve
)
})
}
else
if
(
this
.
type
===
PortForwardType
.
Dynamic
)
{
return
new
Promise
((
resolve
,
reject
)
=>
{
this
.
listener
=
socksv5
.
createServer
((
info
,
accept
,
reject
)
=>
{
callback
(
()
=>
accept
(
true
),
()
=>
reject
(),
null
,
null
,
info
.
dstAddr
,
info
.
dstPort
,
)
})
this
.
listener
.
on
(
'
error
'
,
reject
)
this
.
listener
.
listen
(
this
.
port
,
this
.
host
,
resolve
)
;(
this
.
listener
as
any
).
useAuth
(
socksv5
.
auth
.
None
())
})
}
else
{
throw
new
Error
(
'
Invalid forward type for a local listener
'
)
}
}
stopLocalListener
():
void
{
...
...
@@ -71,8 +100,10 @@ export class ForwardedPort {
toString
():
string
{
if
(
this
.
type
===
PortForwardType
.
Local
)
{
return
`(local)
${
this
.
host
}
:
${
this
.
port
}
→ (remote)
${
this
.
targetAddress
}
:
${
this
.
targetPort
}
`
}
else
{
}
if
(
this
.
type
===
PortForwardType
.
Remote
)
{
return
`(remote)
${
this
.
host
}
:
${
this
.
port
}
→ (local)
${
this
.
targetAddress
}
:
${
this
.
targetPort
}
`
}
else
{
return
`(dynamic)
${
this
.
host
}
:
${
this
.
port
}
`
}
}
}
...
...
@@ -232,25 +263,26 @@ export class SSHSession extends BaseSession {
emitServiceMessage
(
msg
:
string
):
void
{
this
.
serviceMessage
.
next
(
msg
)
this
.
logger
.
info
(
msg
)
this
.
logger
.
info
(
stripAnsi
(
msg
)
)
}
async
addPortForward
(
fw
:
ForwardedPort
):
Promise
<
void
>
{
if
(
fw
.
type
===
PortForwardType
.
Local
)
{
await
fw
.
startLocalListener
((
socket
:
Socke
t
)
=>
{
if
(
fw
.
type
===
PortForwardType
.
Local
||
fw
.
type
===
PortForwardType
.
Dynamic
)
{
await
fw
.
startLocalListener
((
accept
,
reject
,
sourceAddress
,
sourcePort
,
targetAddress
,
targetPor
t
)
=>
{
this
.
logger
.
info
(
`New connection on
${
fw
}
`
)
this
.
ssh
.
forwardOut
(
so
cket
.
remot
eAddress
||
'
127.0.0.1
'
,
so
cket
.
remot
ePort
||
0
,
fw
.
targetAddress
,
fw
.
targetPort
,
so
urc
eAddress
||
'
127.0.0.1
'
,
so
urc
ePort
||
0
,
targetAddress
,
targetPort
,
(
err
,
stream
)
=>
{
if
(
err
)
{
this
.
emitServiceMessage
(
colors
.
bgRed
.
black
(
'
X
'
)
+
` Remote has rejected the forwarded connection via
${
fw
}
:
${
err
}
`
)
socket
.
destroy
()
this
.
emitServiceMessage
(
colors
.
bgRed
.
black
(
'
X
'
)
+
` Remote has rejected the forwarded connection
to
${
targetAddress
}
:
${
targetPort
}
via
${
fw
}
:
${
err
}
`
)
reject
()
return
}
if
(
stream
)
{
const
socket
=
accept
()
stream
.
pipe
(
socket
)
socket
.
pipe
(
stream
)
stream
.
on
(
'
close
'
,
()
=>
{
...
...
@@ -286,7 +318,7 @@ export class SSHSession extends BaseSession {
}
async
removePortForward
(
fw
:
ForwardedPort
):
Promise
<
void
>
{
if
(
fw
.
type
===
PortForwardType
.
Local
)
{
if
(
fw
.
type
===
PortForwardType
.
Local
||
fw
.
type
===
PortForwardType
.
Dynamic
)
{
fw
.
stopLocalListener
()
this
.
forwardedPorts
=
this
.
forwardedPorts
.
filter
(
x
=>
x
!==
fw
)
}
...
...
terminus-ssh/src/components/sshPortForwardingModal.component.pug
浏览文件 @
0971a85d
...
...
@@ -6,12 +6,22 @@
.list-group-item.d-flex.align-items-center(*ngFor='let fw of session.forwardedPorts')
strong(*ngIf='fw.type === PortForwardType.Local') Local
strong(*ngIf='fw.type === PortForwardType.Remote') Remote
.ml-3 {{fw.host}}:{{fw.port}} → {{fw.targetAddress}}:{{fw.targetPort}}
strong(*ngIf='fw.type === PortForwardType.Dynamic') Dynamic
.ml-3 {{fw.host}}:{{fw.port}}
.ml-2 →
.ml-2(*ngIf='fw.type !== PortForwardType.Dynamic') {{fw.targetAddress}}:{{fw.targetPort}}
.ml-2(*ngIf='fw.type === PortForwardType.Dynamic') SOCKS proxy
button.btn.btn-link.ml-auto((click)='remove(fw)')
i.fas.fa-trash-alt.mr-2
span Remove
.input-group.mb-2
.input-group.mb-2(*ngIf='newForward.type === PortForwardType.Dynamic')
input.form-control(type='text', [(ngModel)]='newForward.host')
.input-group-append
.input-group-text :
input.form-control(type='number', [(ngModel)]='newForward.port')
.input-group.mb-2(*ngIf='newForward.type !== PortForwardType.Dynamic')
input.form-control(type='text', [(ngModel)]='newForward.host')
.input-group-append
.input-group-text :
...
...
@@ -42,6 +52,13 @@
[value]='PortForwardType.Remote'
)
| Remote
label.btn.btn-secondary.m-0(ngbButtonLabel)
input(
type='radio',
ngbButton,
[value]='PortForwardType.Dynamic'
)
| Dynamic
button.btn.btn-primary((click)='addForward()')
i.fas.fa-check.mr-2
...
...
terminus-ssh/src/services/ssh.service.ts
浏览文件 @
0971a85d
import
colors
from
'
ansi-colors
'
import
stripAnsi
from
'
strip-ansi
'
import
{
open
as
openTemp
}
from
'
temp
'
import
{
Injectable
,
NgZone
}
from
'
@angular/core
'
import
{
NgbModal
}
from
'
@ng-bootstrap/ng-bootstrap
'
...
...
@@ -137,7 +138,7 @@ export class SSHService {
const
log
=
(
s
:
any
)
=>
{
logCallback
!
(
s
)
this
.
logger
.
info
(
s
)
this
.
logger
.
info
(
s
tripAnsi
(
s
)
)
}
let
privateKey
:
string
|
null
=
null
...
...
terminus-ssh/webpack.config.js
浏览文件 @
0971a85d
...
...
@@ -50,6 +50,7 @@ module.exports = {
'
keytar
'
,
'
path
'
,
'
ngx-toastr
'
,
'
socksv5
'
,
'
windows-native-registry
'
,
'
windows-process-tree/build/Release/windows_process_tree.node
'
,
/^rxjs/
,
...
...
terminus-ssh/yarn.lock
浏览文件 @
0971a85d
...
...
@@ -32,6 +32,11 @@ ansi-colors@^4.1.1:
resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348"
integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==
ansi-regex@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75"
integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==
asn1@~0.2.0, asn1@~0.2.3:
version "0.2.4"
resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136"
...
...
@@ -44,6 +49,11 @@ assert-plus@^1.0.0:
resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525"
integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=
async@0.2.x:
version "0.2.10"
resolved "https://registry.yarnpkg.com/async/-/async-0.2.10.tgz#b6bbe0b0674b9d719708ca38de8c237cb526c3d1"
integrity sha1-trvgsGdLnXGXCMo43owjfLUmw9E=
balanced-match@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
...
...
@@ -69,11 +79,42 @@ cli-spinner@^0.2.10:
resolved "https://registry.yarnpkg.com/cli-spinner/-/cli-spinner-0.2.10.tgz#f7d617a36f5c47a7bc6353c697fc9338ff782a47"
integrity sha512-U0sSQ+JJvSLi1pAYuJykwiA8Dsr15uHEy85iCJ6A+0DjVxivr3d+N2Wjvodeg89uP5K6TswFkKBfAD7B3YSn/Q==
cli@0.4.x:
version "0.4.5"
resolved "https://registry.yarnpkg.com/cli/-/cli-0.4.5.tgz#78f9485cd161b566e9a6c72d7170c4270e81db61"
integrity sha1-ePlIXNFhtWbppsctcXDEJw6B22E=
dependencies:
glob ">= 3.1.4"
cliff@0.1.x:
version "0.1.10"
resolved "https://registry.yarnpkg.com/cliff/-/cliff-0.1.10.tgz#53be33ea9f59bec85609ee300ac4207603e52013"
integrity sha1-U74z6p9ZvshWCe4wCsQgdgPlIBM=
dependencies:
colors "~1.0.3"
eyes "~0.1.8"
winston "0.8.x"
colors@0.6.x:
version "0.6.2"
resolved "https://registry.yarnpkg.com/colors/-/colors-0.6.2.tgz#2423fe6678ac0c5dae8852e5d0e5be08c997abcc"
integrity sha1-JCP+ZnisDF2uiFLl0OW+CMmXq8w=
colors@~1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b"
integrity sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs=
concat-map@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
cycle@1.0.x:
version "1.0.3"
resolved "https://registry.yarnpkg.com/cycle/-/cycle-1.0.3.tgz#21e80b2be8580f98b468f379430662b046c34ad2"
integrity sha1-IegLK+hYD5i0aPN5QwZisEbDStI=
dashdash@^1.12.0:
version "1.14.1"
resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0"
...
...
@@ -89,6 +130,11 @@ ecc-jsbn@~0.1.1:
jsbn "~0.1.0"
safer-buffer "^2.1.0"
eyes@0.1.x, eyes@~0.1.8:
version "0.1.8"
resolved "https://registry.yarnpkg.com/eyes/-/eyes-0.1.8.tgz#62cf120234c683785d902348a800ef3e0cc20bc0"
integrity sha1-Ys8SAjTGg3hdkCNIqADvPgzCC8A=
fs.realpath@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
...
...
@@ -101,7 +147,7 @@ getpass@^0.1.1:
dependencies:
assert-plus "^1.0.0"
glob@^7.1.3:
"glob@>= 3.1.4",
glob@^7.1.3:
version "7.1.6"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6"
integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==
...
...
@@ -126,6 +172,20 @@ inherits@2:
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
ipv6@*:
version "3.1.3"
resolved "https://registry.yarnpkg.com/ipv6/-/ipv6-3.1.3.tgz#4d9064f9c2dafa0dd10b8b7d76ffca4aad31b3b9"
integrity sha1-TZBk+cLa+g3RC4t9dv/KSq0xs7k=
dependencies:
cli "0.4.x"
cliff "0.1.x"
sprintf "0.1.x"
isstream@0.1.x:
version "0.1.2"
resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=
jsbn@~0.1.0:
version "0.1.1"
resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
...
...
@@ -150,6 +210,11 @@ path-is-absolute@^1.0.0:
resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18=
pkginfo@0.3.x:
version "0.3.1"
resolved "https://registry.yarnpkg.com/pkginfo/-/pkginfo-0.3.1.tgz#5b29f6a81f70717142e09e765bbeab97b4f81e21"
integrity sha1-Wyn2qB9wcXFC4J52W76rl7T4HiE=
rimraf@~2.6.2:
version "2.6.3"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab"
...
...
@@ -167,6 +232,18 @@ safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0:
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
socksv5@^0.0.6:
version "0.0.6"
resolved "https://registry.yarnpkg.com/socksv5/-/socksv5-0.0.6.tgz#1327235ff7e8de21ac434a0a579dc69c3f071061"
integrity sha1-EycjX/fo3iGsQ0oKV53GnD8HEGE=
dependencies:
ipv6 "*"
sprintf@0.1.x:
version "0.1.5"
resolved "https://registry.yarnpkg.com/sprintf/-/sprintf-0.1.5.tgz#8f83e39a9317c1a502cb7db8050e51c679f6edcf"
integrity sha1-j4PjmpMXwaUCy324BQ5Rxnn27c8=
ssh2-streams@Eugeny/ssh2-streams#75f6d3425d071ac73a18fd46e2f5e738bfe897c5:
version "0.4.10"
resolved "https://codeload.github.com/Eugeny/ssh2-streams/tar.gz/75f6d3425d071ac73a18fd46e2f5e738bfe897c5"
...
...
@@ -184,7 +261,7 @@ ssh2-streams@~0.4.10:
bcrypt-pbkdf "^1.0.2"
streamsearch "~0.1.2"
ssh2@^0.8.
2
:
ssh2@^0.8.
9
:
version "0.8.9"
resolved "https://registry.yarnpkg.com/ssh2/-/ssh2-0.8.9.tgz#54da3a6c4ba3daf0d8477a538a481326091815f3"
integrity sha512-GmoNPxWDMkVpMFa9LVVzQZHF6EW3WKmBwL+4/GeILf2hFmix5Isxm7Amamo8o7bHiU0tC+wXsGcUXOxp8ChPaw==
...
...
@@ -206,11 +283,23 @@ sshpk@^1.16.1:
safer-buffer "^2.0.2"
tweetnacl "~0.14.0"
stack-trace@0.0.x:
version "0.0.10"
resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0"
integrity sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=
streamsearch@~0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-0.1.2.tgz#808b9d0e56fc273d809ba57338e929919a1a9f1a"
integrity sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=
strip-ansi@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532"
integrity sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==
dependencies:
ansi-regex "^5.0.0"
temp@^0.9.1:
version "0.9.1"
resolved "https://registry.yarnpkg.com/temp/-/temp-0.9.1.tgz#2d666114fafa26966cd4065996d7ceedd4dd4697"
...
...
@@ -228,6 +317,19 @@ tweetnacl@^0.14.3, tweetnacl@~0.14.0:
resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"
integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=
winston@0.8.x:
version "0.8.3"
resolved "https://registry.yarnpkg.com/winston/-/winston-0.8.3.tgz#64b6abf4cd01adcaefd5009393b1d8e8bec19db0"
integrity sha1-ZLar9M0Brcrv1QCTk7HY6L7BnbA=
dependencies:
async "0.2.x"
colors "0.6.x"
cycle "1.0.x"
eyes "0.1.x"
isstream "0.1.x"
pkginfo "0.3.x"
stack-trace "0.0.x"
wrappy@1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
...
...
编辑
预览
Markdown
is supported
0%
请重试
或
添加新附件
.
添加附件
取消
You are about to add
0
people
to the discussion. Proceed with caution.
先完成此消息的编辑!
取消
想要评论请
注册
或
登录