Compare commits
9 Commits
4590fdadbe
...
v0.0.1
| Author | SHA1 | Date | |
|---|---|---|---|
| 1d292ec21a | |||
| ee828c3e24 | |||
| a9fdf0a053 | |||
| ca02d9b609 | |||
| 792dfdba78 | |||
| 4f7a8f0b04 | |||
| 6d6ed30227 | |||
| 2817417eca | |||
| fa60e18c22 |
70
.goreleaser.yaml
Normal file
70
.goreleaser.yaml
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
before:
|
||||||
|
hooks:
|
||||||
|
- go mod tidy
|
||||||
|
builds:
|
||||||
|
- id: seashell
|
||||||
|
env:
|
||||||
|
- CGO_ENABLED=0
|
||||||
|
binary: seashell
|
||||||
|
goos:
|
||||||
|
- linux
|
||||||
|
goarch:
|
||||||
|
- amd64
|
||||||
|
- "386"
|
||||||
|
- arm64
|
||||||
|
- arm
|
||||||
|
- riscv64
|
||||||
|
archives:
|
||||||
|
- files:
|
||||||
|
- seashell.service
|
||||||
|
nfpms:
|
||||||
|
- id: seashell
|
||||||
|
description: "SSH server with virtual hosts and username-based routing"
|
||||||
|
homepage: 'https://gitea.elara.ws/Elara6331/seashell'
|
||||||
|
maintainer: 'Elara Ivy <elara@elara.ws>'
|
||||||
|
license: AGPLv3
|
||||||
|
formats:
|
||||||
|
- deb
|
||||||
|
- rpm
|
||||||
|
- apk
|
||||||
|
- archlinux
|
||||||
|
provides:
|
||||||
|
- seashell
|
||||||
|
conflicts:
|
||||||
|
- seashell
|
||||||
|
contents:
|
||||||
|
- src: seashell.service
|
||||||
|
dst: /etc/systemd/system/seashell.service
|
||||||
|
aurs:
|
||||||
|
- name: seashell-bin
|
||||||
|
description: "SSH server with virtual hosts and username-based routing"
|
||||||
|
homepage: 'https://gitea.elara.ws/Elara6331/seashell'
|
||||||
|
maintainers:
|
||||||
|
- 'Elara Ivy <elara@elara.ws>'
|
||||||
|
license: AGPLv3
|
||||||
|
private_key: '{{ .Env.AUR_KEY }}'
|
||||||
|
git_url: 'ssh://aur@aur.archlinux.org/seashell-bin.git'
|
||||||
|
provides:
|
||||||
|
- seashell
|
||||||
|
conflicts:
|
||||||
|
- seashell
|
||||||
|
package: |-
|
||||||
|
# binaries
|
||||||
|
install -Dm755 ./seashell "${pkgdir}/usr/bin/seashell"
|
||||||
|
|
||||||
|
# services
|
||||||
|
install -Dm644 ./seashell.service "${pkgdir}/etc/systemd/system/seashell.service"
|
||||||
|
release:
|
||||||
|
gitea:
|
||||||
|
owner: Elara6331
|
||||||
|
name: seashell
|
||||||
|
gitea_urls:
|
||||||
|
api: 'https://gitea.elara.ws/api/v1/'
|
||||||
|
download: 'https://gitea.elara.ws'
|
||||||
|
skip_tls_verify: false
|
||||||
|
checksum:
|
||||||
|
name_template: 'checksums.txt'
|
||||||
|
snapshot:
|
||||||
|
name_template: "{{ incpatch .Version }}-next"
|
||||||
|
changelog:
|
||||||
|
sort: asc
|
||||||
25
.woodpecker.yml
Normal file
25
.woodpecker.yml
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
labels:
|
||||||
|
platform: linux/amd64
|
||||||
|
|
||||||
|
steps:
|
||||||
|
docker:
|
||||||
|
image: gitea.elara.ws/elara6331/builder
|
||||||
|
environment:
|
||||||
|
- REGISTRY=gitea.elara.ws
|
||||||
|
- REGISTRY_USERNAME=Elara6331
|
||||||
|
- KO_DOCKER_REPO=gitea.elara.ws/elara6331
|
||||||
|
- KO_DEFAULTBASEIMAGE=gitea.elara.ws/elara6331/static
|
||||||
|
secrets: [ registry_password ]
|
||||||
|
commands:
|
||||||
|
- registry-login
|
||||||
|
- ko build -B --platform=linux/amd64,linux/arm64,linux/riscv64 -t latest,${CI_COMMIT_TAG} --sbom=none
|
||||||
|
when:
|
||||||
|
event: tag
|
||||||
|
|
||||||
|
release:
|
||||||
|
image: goreleaser/goreleaser
|
||||||
|
commands:
|
||||||
|
- goreleaser release
|
||||||
|
secrets: [ gitea_token, aur_key ]
|
||||||
|
when:
|
||||||
|
event: tag
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="assets/seashell-text.svg" width="250">
|
<img src="assets/seashell-text.svg" width="250" alt="Seashell logo"><br><br>
|
||||||
|
<a href="https://goreportcard.com/report/go.elara.ws/seashell"><img src="https://goreportcard.com/badge/go.elara.ws/seashell?style=for-the-badge" alt="Go Report Card"></a>
|
||||||
|
<a href="https://gitea.elara.ws/Elara6331/seashell/wiki/Home"><img src="https://img.shields.io/badge/read%20the-docs-purple?style=for-the-badge" alt="Read the Docs"></a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
21
auth.go
21
auth.go
@@ -1,3 +1,24 @@
|
|||||||
|
/*
|
||||||
|
* Seashell - SSH server with virtual hosts and username-based routing
|
||||||
|
*
|
||||||
|
* Copyright (C) 2024 Elara6331 <elara@elara.ws>
|
||||||
|
*
|
||||||
|
* This file is part of Seashell.
|
||||||
|
*
|
||||||
|
* Seashell 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.
|
||||||
|
*
|
||||||
|
* Seashell 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 Seashell. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -22,8 +22,6 @@
|
|||||||
package backends
|
package backends
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/zclconf/go-cty/cty"
|
"github.com/zclconf/go-cty/cty"
|
||||||
"go.elara.ws/seashell/internal/config"
|
"go.elara.ws/seashell/internal/config"
|
||||||
"go.elara.ws/seashell/internal/router"
|
"go.elara.ws/seashell/internal/router"
|
||||||
@@ -84,17 +82,6 @@ func ctyObjToStringMap(o *cty.Value) map[string]string {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
// sshGetenv gets an environment variable from the SSH session
|
|
||||||
func sshGetenv(env []string, key string) string {
|
|
||||||
for _, kv := range env {
|
|
||||||
before, after, ok := strings.Cut(kv, "=")
|
|
||||||
if ok && before == key {
|
|
||||||
return after
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// valueOr returns the value that v points to
|
// valueOr returns the value that v points to
|
||||||
// or a default value if v is nil.
|
// or a default value if v is nil.
|
||||||
func valueOr[T any](v *T, or T) T {
|
func valueOr[T any](v *T, or T) T {
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ type dockerSettings struct {
|
|||||||
Command *cty.Value `cty:"command"`
|
Command *cty.Value `cty:"command"`
|
||||||
Privileged *bool `cty:"privileged"`
|
Privileged *bool `cty:"privileged"`
|
||||||
User *string `cty:"user"`
|
User *string `cty:"user"`
|
||||||
|
UserMap *cty.Value `cty:"user_map"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Docker is the docker backend. It returns a handler that connects
|
// Docker is the docker backend. It returns a handler that connects
|
||||||
@@ -62,6 +63,17 @@ func Docker(route config.Route) router.Handler {
|
|||||||
if !ok {
|
if !ok {
|
||||||
return errors.New("this route only accepts pty sessions (try adding the -t flag)")
|
return errors.New("this route only accepts pty sessions (try adding the -t flag)")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if opts.User == nil {
|
||||||
|
userMap := ctyObjToStringMap(opts.UserMap)
|
||||||
|
user, _ := sshctx.GetUser(sess.Context())
|
||||||
|
|
||||||
|
if muser, ok := userMap[user.Name]; ok {
|
||||||
|
opts.User = &muser
|
||||||
|
} else {
|
||||||
|
opts.User = &user.Name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
c, err := client.NewClientWithOpts(
|
c, err := client.NewClientWithOpts(
|
||||||
client.WithHostFromEnv(),
|
client.WithHostFromEnv(),
|
||||||
@@ -72,11 +84,6 @@ func Docker(route config.Route) router.Handler {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if opts.User == nil {
|
|
||||||
envUser := sshGetenv(sess.Environ(), "DOCKER_USER")
|
|
||||||
opts.User = &envUser
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd := sess.Command()
|
cmd := sess.Command()
|
||||||
if len(cmd) == 0 {
|
if len(cmd) == 0 {
|
||||||
cmd = ctyTupleToStrings(opts.Command)
|
cmd = ctyTupleToStrings(opts.Command)
|
||||||
|
|||||||
@@ -150,7 +150,7 @@ func Nomad(route config.Route) router.Handler {
|
|||||||
return errors.New("task group not found")
|
return errors.New("task group not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
var taskName = args[2]
|
taskName := args[2]
|
||||||
if taskName == "" {
|
if taskName == "" {
|
||||||
taskName = group.Tasks[0].Name
|
taskName = group.Tasks[0].Name
|
||||||
}
|
}
|
||||||
@@ -189,7 +189,7 @@ func Nomad(route config.Route) router.Handler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var taskName = args[3]
|
taskName := args[3]
|
||||||
if taskName == "" {
|
if taskName == "" {
|
||||||
taskName = group.Tasks[0].Name
|
taskName = group.Tasks[0].Name
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,12 +42,13 @@ import (
|
|||||||
// proxySettings represents settings for the proxy backend.
|
// proxySettings represents settings for the proxy backend.
|
||||||
type proxySettings struct {
|
type proxySettings struct {
|
||||||
Server string `cty:"server"`
|
Server string `cty:"server"`
|
||||||
|
Port *uint `cty:"port"`
|
||||||
User *string `cty:"user"`
|
User *string `cty:"user"`
|
||||||
PrivkeyPath *string `cty:"privkey"`
|
PrivkeyPath *string `cty:"privkey"`
|
||||||
UserMap *cty.Value `cty:"userMap"`
|
UserMap *cty.Value `cty:"user_map"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Proxy is the proxy backend. It returns a handler that establishes a proxy
|
// Proxy is the proxy backend. It returns a handler that establishes a proxy
|
||||||
// session to a remote server based on the provided configuration.
|
// session to a remote server based on the provided configuration.
|
||||||
func Proxy(route config.Route) router.Handler {
|
func Proxy(route config.Route) router.Handler {
|
||||||
return func(sess ssh.Session, arg string) error {
|
return func(sess ssh.Session, arg string) error {
|
||||||
@@ -70,13 +71,18 @@ func Proxy(route config.Route) router.Handler {
|
|||||||
if opts.User == nil {
|
if opts.User == nil {
|
||||||
userMap := ctyObjToStringMap(opts.UserMap)
|
userMap := ctyObjToStringMap(opts.UserMap)
|
||||||
user, _ := sshctx.GetUser(sess.Context())
|
user, _ := sshctx.GetUser(sess.Context())
|
||||||
|
|
||||||
if muser, ok := userMap[user.Name]; ok {
|
if muser, ok := userMap[user.Name]; ok {
|
||||||
opts.User = &muser
|
opts.User = &muser
|
||||||
} else {
|
} else {
|
||||||
opts.User = &user.Name
|
opts.User = &user.Name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if opts.Port == nil {
|
||||||
|
port := uint(22)
|
||||||
|
opts.Port = &port
|
||||||
|
}
|
||||||
|
|
||||||
auth := goph.Auth{
|
auth := goph.Auth{
|
||||||
gossh.PasswordCallback(requestPassword(opts, sess)),
|
gossh.PasswordCallback(requestPassword(opts, sess)),
|
||||||
@@ -96,24 +102,26 @@ func Proxy(route config.Route) router.Handler {
|
|||||||
auth = append(goph.Auth{gossh.PublicKeys(pk)}, auth...)
|
auth = append(goph.Auth{gossh.PublicKeys(pk)}, auth...)
|
||||||
}
|
}
|
||||||
|
|
||||||
c, err := goph.New(*opts.User, opts.Server, auth)
|
c, err := goph.NewConn(&goph.Config{
|
||||||
|
Auth: auth,
|
||||||
|
User: *opts.User,
|
||||||
|
Addr: opts.Server,
|
||||||
|
Port: *opts.Port,
|
||||||
|
Callback: func(host string, remote net.Addr, key gossh.PublicKey) error {
|
||||||
|
found, err := goph.CheckKnownHost(host, remote, key, "")
|
||||||
|
if !found {
|
||||||
|
if err = goph.AddKnownHost(host, remote, key, ""); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
knownHostHandler, err := goph.DefaultKnownHosts()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
c.Config.Callback = func(host string, remote net.Addr, key gossh.PublicKey) error {
|
|
||||||
println("hi")
|
|
||||||
err = goph.AddKnownHost(host, remote, key, "")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return knownHostHandler(host, remote, key)
|
|
||||||
}
|
|
||||||
|
|
||||||
baseCmd := sess.Command()
|
baseCmd := sess.Command()
|
||||||
|
|
||||||
@@ -187,7 +195,7 @@ func sshHandleResize(resizeCh <-chan ssh.Window, cmd *goph.Cmd) {
|
|||||||
|
|
||||||
// readPassword reads a password from the SSH session, sending an asterisk
|
// readPassword reads a password from the SSH session, sending an asterisk
|
||||||
// for each character typed.
|
// for each character typed.
|
||||||
//
|
//
|
||||||
// It handles interrupts (Ctrl+C), EOF (Ctrl+D), and backspace.
|
// It handles interrupts (Ctrl+C), EOF (Ctrl+D), and backspace.
|
||||||
// It returns what it read once it receives a carriage return or a newline.
|
// It returns what it read once it receives a carriage return or a newline.
|
||||||
func readPassword(sess ssh.Session) (string, error) {
|
func readPassword(sess ssh.Session) (string, error) {
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ func Serial(route config.Route) router.Handler {
|
|||||||
file, baudRate, config = filepath.Join(*opts.Directory, args[0]), args[1], args[2]
|
file, baudRate, config = filepath.Join(*opts.Directory, args[0]), args[1], args[2]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !route.Permissions.IsAllowed(user, filepath.Base(file)) {
|
if !route.Permissions.IsAllowed(user, filepath.Base(file)) {
|
||||||
return router.ErrUnauthorized
|
return router.ErrUnauthorized
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ import (
|
|||||||
type PermissionsMap map[string]map[string][]string
|
type PermissionsMap map[string]map[string][]string
|
||||||
|
|
||||||
// IsAllowed checks if the user has permissions for all the specified items.
|
// IsAllowed checks if the user has permissions for all the specified items.
|
||||||
//
|
//
|
||||||
// The default policy is deny, and denials take priority, so if one item
|
// The default policy is deny, and denials take priority, so if one item
|
||||||
// in items is set to deny, IsAllowed will always return false, even if
|
// in items is set to deny, IsAllowed will always return false, even if
|
||||||
// other items are explicitly allowed.
|
// other items are explicitly allowed.
|
||||||
|
|||||||
21
keys.go
21
keys.go
@@ -1,3 +1,24 @@
|
|||||||
|
/*
|
||||||
|
* Seashell - SSH server with virtual hosts and username-based routing
|
||||||
|
*
|
||||||
|
* Copyright (C) 2024 Elara6331 <elara@elara.ws>
|
||||||
|
*
|
||||||
|
* This file is part of Seashell.
|
||||||
|
*
|
||||||
|
* Seashell 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.
|
||||||
|
*
|
||||||
|
* Seashell 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 Seashell. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
11
seashell.service
Normal file
11
seashell.service
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Seashell SSH Server
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
ExecStart=seashell
|
||||||
|
Restart=always
|
||||||
|
StandardOutput=journal
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
Reference in New Issue
Block a user