task3 backend

This commit is contained in:
2020-01-04 12:38:50 +01:00
parent 6cfe7d338a
commit 728c0273d9
10 changed files with 829 additions and 0 deletions
+71
View File
@@ -0,0 +1,71 @@
package lib
import (
"context"
"net/http"
"strings"
"github.com/docker/docker/client"
"github.com/go-redis/redis/v7"
routing "github.com/jackwhelpton/fasthttp-routing/v2"
"github.com/jackwhelpton/fasthttp-routing/v2/access"
"github.com/jackwhelpton/fasthttp-routing/v2/cors"
"github.com/jackwhelpton/fasthttp-routing/v2/fault"
log "github.com/sirupsen/logrus"
"github.com/valyala/fasthttp"
)
const (
URLHealthz = "/healthz"
)
type Api struct {
Base string
Docker *client.Client
Redis *redis.Client
}
func (a *Api) GetHandler() fasthttp.RequestHandler {
crs := cors.Options{
AllowOrigins: "*",
AllowHeaders: "*",
AllowMethods: "*",
AllowCredentials: true,
}
router := routing.New()
router.Use(
access.Logger(log.Debugf),
cors.Handler(crs),
fault.PanicHandler(log.Warnf),
)
base := strings.TrimSuffix(a.Base, "/")
api := router.Group(base)
api.Get(URLHealthz, a.healthCheck)
return router.HandleRequest
}
func (a *Api) healthCheck(ctx *routing.Context) (err error) {
_, err = a.Redis.Ping().Result()
if err != nil {
ctx.SetStatusCode(http.StatusInternalServerError)
return
}
_, err = a.Docker.Ping(context.Background())
if err != nil {
ctx.SetStatusCode(http.StatusInternalServerError)
return
}
ctx.Response.Header.Set("Content-Type", "application/json")
_, err = ctx.WriteString("{\"health\":\"ok\"}")
return
}
+22
View File
@@ -0,0 +1,22 @@
package lib
var (
ConfPath = "config"
ConfName = "task3"
)
type AppConfig struct {
HttpAPI struct {
Base string `mapstructure:"base"`
Addr string `mapstructure:"addr"`
} `mapstructure:"httpApi"`
Task struct {
Addr string `mapstructure:"addr"`
Clients int `mapstructure:"clients"`
} `mapstructure:"task"`
Redis struct {
Addr string `mapstructure:"addr"`
ScoreKey string `mapstructure:"scoreKey"`
WinnersKey string `mapstructure:"winnersKey"`
} `mapstructure:"redis"`
}
+38
View File
@@ -0,0 +1,38 @@
package configure
import (
"strings"
"github.com/sirupsen/logrus"
"github.com/spf13/viper"
"github.com/mainnika/a-quest/task3-backend/lib"
"github.com/mainnika/a-quest/task3-backend/lib/env"
)
var Config lib.AppConfig
func init() {
path := env.ConfigPath
name := env.ConfigName
if len(path) == 0 {
path = lib.ConfPath
}
if len(name) == 0 {
name = lib.ConfName
}
viper.AddConfigPath(path)
viper.SetConfigName(name)
viper.SetEnvPrefix(env.Prefix)
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
viper.AutomaticEnv()
err, _ := viper.ReadInConfig(), viper.Unmarshal(&Config)
if err != nil {
logrus.Fatal(err)
}
}
+13
View File
@@ -0,0 +1,13 @@
package env
import (
"fmt"
"os"
)
var (
Prefix = "CFG"
IsDevelopment = len(os.Getenv("DEBUG")) > 0
ConfigPath = os.Getenv(fmt.Sprintf("%s_PATH", Prefix))
ConfigName = os.Getenv(fmt.Sprintf("%s_NAME", Prefix))
)
+309
View File
@@ -0,0 +1,309 @@
package lib
import (
"bufio"
"context"
"crypto/ecdsa"
"errors"
"fmt"
"net"
"sync/atomic"
"time"
jwtgo "github.com/dgrijalva/jwt-go"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/mount"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/client"
"github.com/go-redis/redis/v7"
log "github.com/sirupsen/logrus"
)
type Server struct {
LetterPath string
Alg string
Pub *ecdsa.PublicKey
Priv *ecdsa.PrivateKey
Docker *client.Client
WinnersKey string
Redis *redis.Client
ClientsLimit uint32
connected uint32
}
func (s *Server) Serve(lis net.Listener) (err error) {
for {
var conn net.Conn
conn, err = lis.Accept()
if err != nil {
return
}
go s.HandleClient(conn)
}
}
func (s *Server) HandleClient(conn net.Conn) {
var err error
defer log.Debugf("done client %v", conn)
defer atomic.AddUint32(&s.connected, ^uint32(0))
defer func() {
if err != nil {
log.Debug(err)
}
_ = conn.Close()
}()
log.Debugf("new client %v", conn)
connected := atomic.AddUint32(&s.connected, 1)
if connected > s.ClientsLimit {
return
}
now := time.Now().UnixNano()
claims := &jwtgo.StandardClaims{}
go func() {
time.Sleep(time.Minute)
err = conn.Close()
}()
key, err := s.readKey(conn)
if err != nil {
return
}
_, err = jwtgo.ParseWithClaims(key, claims, s.getKey)
if err != nil {
return
}
if !s.isWinner(claims.Id) {
return
}
log.Debugf("connected with id %s", claims.Id)
jailId, err := s.createJail(now, claims)
if err != nil {
return
}
defer s.removeJailNoErr(jailId)
log.Debugf("jail created %s → %s", claims.Id, jailId)
err = s.startJail(jailId)
if err != nil {
return
}
jailConn, err := s.attachJail(jailId)
if err != nil {
return
}
greetings := fmt.Sprintf("welcome %s! I have a letter for you, just read it!", claims.Subject)
cmd := fmt.Sprintf(`echo %s 2>&1 | tee msg /dev/console`, greetings)
err = s.execJail(jailId, []string{"sh", "-c", cmd})
if err != nil {
return
}
err = s.startProxyJail(jailConn, conn)
if err != nil {
return
}
}
func (s *Server) getPrivKey(t *jwtgo.Token) (interface{}, error) {
if _, ok := t.Method.(*jwtgo.SigningMethodECDSA); !ok {
return nil, errors.New("unexpected signing method")
}
return s.Priv, nil
}
func (s *Server) getKey(t *jwtgo.Token) (interface{}, error) {
if _, ok := t.Method.(*jwtgo.SigningMethodECDSA); !ok {
return nil, errors.New("unexpected signing method")
}
return s.Pub, nil
}
func (s *Server) isWinner(id string) (winner bool) {
winner, _ = s.Redis.SIsMember(s.WinnersKey, id).Result()
return
}
func (s *Server) readKey(conn net.Conn) (key string, err error) {
buf := make([]byte, 4096)
scanner := bufio.NewScanner(conn)
scanner.Buffer(buf, 0)
for {
err = scanner.Err()
if err != nil {
return
}
if !scanner.Scan() {
continue
}
key = scanner.Text()
break
}
return
}
func (s *Server) createJail(now int64, claims *jwtgo.StandardClaims) (id string, err error) {
name := fmt.Sprintf("%d-%s", now, claims.Id)
cfg := &container.Config{
NetworkDisabled: true,
Labels: map[string]string{
"id": claims.Id,
},
Image: "alpine",
User: "nobody",
OpenStdin: true,
Tty: true,
Cmd: []string{"/bin/sh"},
}
letter := mount.Mount{
Type: mount.TypeBind,
Source: s.LetterPath,
Target: "/letter",
ReadOnly: true,
}
hcfg := &container.HostConfig{
NetworkMode: "none",
Mounts: []mount.Mount{letter},
}
ncfg := &network.NetworkingConfig{}
created, err := s.Docker.ContainerCreate(context.Background(), cfg, hcfg, ncfg, name)
if err != nil {
return
}
id = created.ID
return
}
func (s *Server) removeJailNoErr(id string) {
_ = s.Docker.ContainerRemove(context.Background(), id, types.ContainerRemoveOptions{Force: true})
}
func (s *Server) startJail(id string) (err error) {
return s.Docker.ContainerStart(context.Background(), id, types.ContainerStartOptions{})
}
func (s *Server) execJail(id string, cmd []string) (err error) {
ctx := context.Background()
exec, err := s.Docker.ContainerExecCreate(ctx, id, types.ExecConfig{User: "root", Cmd: cmd, Detach: true})
if err != nil {
return
}
err = s.Docker.ContainerExecStart(ctx, exec.ID, types.ExecStartCheck{Detach: true})
return
}
func (s *Server) attachJail(id string) (conn net.Conn, err error) {
containerio, err := s.Docker.ContainerAttach(context.Background(), id, types.ContainerAttachOptions{Stdin: true, Stdout: true, Stderr: true, Stream: true})
if err != nil {
return
}
conn = containerio.Conn
return
}
func (s *Server) startProxyJail(jailconn net.Conn, userconn net.Conn) (err error) {
closed := new(uint32)
stdin := createProxyChan(userconn, closed)
stdout := createProxyChan(jailconn, closed)
defer jailconn.Close()
Proxying:
for {
select {
case data, ok := <-stdin:
if !ok {
break Proxying
}
_, err = jailconn.Write(data)
case data, ok := <-stdout:
if !ok {
break Proxying
}
_, err = userconn.Write(data)
}
if err != nil {
break
}
}
return
}
func createProxyChan(conn net.Conn, closed *uint32) (out chan []byte) {
out = make(chan []byte)
b := make([]byte, 1024)
go func() {
defer close(out)
for {
deadline := time.Now().Add(time.Minute)
err := conn.SetReadDeadline(deadline)
if err != nil {
log.Debugf("conn deadline failed: %v", err)
return
}
if !atomic.CompareAndSwapUint32(closed, 0, 0) {
return
}
n, err := conn.Read(b)
if n > 0 {
res := make([]byte, n)
copy(res, b[:n])
out <- res
}
if err != nil {
log.Debugf("conn read failed: %v", err)
return
}
}
}()
return
}