first commit
This commit is contained in:
8
server/internal/config/config.go
Normal file
8
server/internal/config/config.go
Normal file
@ -0,0 +1,8 @@
|
||||
package config
|
||||
|
||||
import "github.com/spf13/cobra"
|
||||
|
||||
type Config interface {
|
||||
Init(cmd *cobra.Command) error
|
||||
Set()
|
||||
}
|
37
server/internal/config/root.go
Normal file
37
server/internal/config/root.go
Normal file
@ -0,0 +1,37 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
type Root struct {
|
||||
Debug bool
|
||||
Logs bool
|
||||
CfgFile string
|
||||
}
|
||||
|
||||
func (Root) Init(cmd *cobra.Command) error {
|
||||
cmd.PersistentFlags().BoolP("debug", "d", false, "Enable debug mode")
|
||||
if err := viper.BindPFlag("debug", cmd.PersistentFlags().Lookup("debug")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().BoolP("logs", "l", false, "Save logs to file")
|
||||
if err := viper.BindPFlag("logs", cmd.PersistentFlags().Lookup("logs")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("config", "", "Configuration file path")
|
||||
if err := viper.BindPFlag("config", cmd.PersistentFlags().Lookup("config")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Root) Set() {
|
||||
s.Logs = viper.GetBool("logs")
|
||||
s.Debug = viper.GetBool("debug")
|
||||
s.CfgFile = viper.GetString("config")
|
||||
}
|
52
server/internal/config/serve.go
Normal file
52
server/internal/config/serve.go
Normal file
@ -0,0 +1,52 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
type Serve struct {
|
||||
Cert string
|
||||
Key string
|
||||
Bind string
|
||||
Password string
|
||||
Static string
|
||||
}
|
||||
|
||||
func (Serve) Init(cmd *cobra.Command) error {
|
||||
|
||||
cmd.PersistentFlags().String("bind", "127.0.0.1:8080", "Address/port/socket to serve neko")
|
||||
if err := viper.BindPFlag("bind", cmd.PersistentFlags().Lookup("bind")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("cert", "", "Path to the SSL cert used to secure the neko server")
|
||||
if err := viper.BindPFlag("cert", cmd.PersistentFlags().Lookup("cert")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("key", "", "Path to the SSL key used to secure the neko server")
|
||||
if err := viper.BindPFlag("key", cmd.PersistentFlags().Lookup("key")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("password", "neko", "Password for connecting to stream")
|
||||
if err := viper.BindPFlag("password", cmd.PersistentFlags().Lookup("password")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("static", "./www", "Static files to serve")
|
||||
if err := viper.BindPFlag("static", cmd.PersistentFlags().Lookup("static")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Serve) Set() {
|
||||
s.Cert = viper.GetString("cert")
|
||||
s.Key = viper.GetString("key")
|
||||
s.Bind = viper.GetString("bind")
|
||||
s.Password = viper.GetString("password")
|
||||
s.Static = viper.GetString("static")
|
||||
}
|
88
server/internal/gst/gst.c
Normal file
88
server/internal/gst/gst.c
Normal file
@ -0,0 +1,88 @@
|
||||
#include "gst.h"
|
||||
|
||||
#include <gst/app/gstappsrc.h>
|
||||
|
||||
typedef struct SampleHandlerUserData {
|
||||
int pipelineId;
|
||||
} SampleHandlerUserData;
|
||||
|
||||
GMainLoop *gstreamer_send_main_loop = NULL;
|
||||
void gstreamer_send_start_mainloop(void) {
|
||||
gstreamer_send_main_loop = g_main_loop_new(NULL, FALSE);
|
||||
|
||||
g_main_loop_run(gstreamer_send_main_loop);
|
||||
}
|
||||
|
||||
static gboolean gstreamer_send_bus_call(GstBus *bus, GstMessage *msg, gpointer data) {
|
||||
switch (GST_MESSAGE_TYPE(msg)) {
|
||||
|
||||
case GST_MESSAGE_EOS:
|
||||
g_print("End of stream\n");
|
||||
exit(1);
|
||||
break;
|
||||
|
||||
case GST_MESSAGE_ERROR: {
|
||||
gchar *debug;
|
||||
GError *error;
|
||||
|
||||
gst_message_parse_error(msg, &error, &debug);
|
||||
g_free(debug);
|
||||
|
||||
g_printerr("Error: %s\n", error->message);
|
||||
g_error_free(error);
|
||||
exit(1);
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
GstFlowReturn gstreamer_send_new_sample_handler(GstElement *object, gpointer user_data) {
|
||||
GstSample *sample = NULL;
|
||||
GstBuffer *buffer = NULL;
|
||||
gpointer copy = NULL;
|
||||
gsize copy_size = 0;
|
||||
SampleHandlerUserData *s = (SampleHandlerUserData *)user_data;
|
||||
|
||||
g_signal_emit_by_name (object, "pull-sample", &sample);
|
||||
if (sample) {
|
||||
buffer = gst_sample_get_buffer(sample);
|
||||
if (buffer) {
|
||||
gst_buffer_extract_dup(buffer, 0, gst_buffer_get_size(buffer), ©, ©_size);
|
||||
goHandlePipelineBuffer(copy, copy_size, GST_BUFFER_DURATION(buffer), s->pipelineId);
|
||||
}
|
||||
gst_sample_unref (sample);
|
||||
}
|
||||
|
||||
return GST_FLOW_OK;
|
||||
}
|
||||
|
||||
GstElement *gstreamer_send_create_pipeline(char *pipeline) {
|
||||
gst_init(NULL, NULL);
|
||||
GError *error = NULL;
|
||||
return gst_parse_launch(pipeline, &error);
|
||||
}
|
||||
|
||||
void gstreamer_send_start_pipeline(GstElement *pipeline, int pipelineId) {
|
||||
SampleHandlerUserData *s = calloc(1, sizeof(SampleHandlerUserData));
|
||||
s->pipelineId = pipelineId;
|
||||
|
||||
GstBus *bus = gst_pipeline_get_bus(GST_PIPELINE(pipeline));
|
||||
gst_bus_add_watch(bus, gstreamer_send_bus_call, NULL);
|
||||
gst_object_unref(bus);
|
||||
|
||||
GstElement *appsink = gst_bin_get_by_name(GST_BIN(pipeline), "appsink");
|
||||
g_object_set(appsink, "emit-signals", TRUE, NULL);
|
||||
g_signal_connect(appsink, "new-sample", G_CALLBACK(gstreamer_send_new_sample_handler), s);
|
||||
gst_object_unref(appsink);
|
||||
|
||||
gst_element_set_state(pipeline, GST_STATE_PLAYING);
|
||||
}
|
||||
|
||||
void gstreamer_send_stop_pipeline(GstElement *pipeline) {
|
||||
gst_element_set_state(pipeline, GST_STATE_NULL);
|
||||
}
|
||||
|
||||
|
125
server/internal/gst/gst.go
Normal file
125
server/internal/gst/gst.go
Normal file
@ -0,0 +1,125 @@
|
||||
package gst
|
||||
|
||||
/*
|
||||
#cgo pkg-config: gstreamer-1.0 gstreamer-app-1.0
|
||||
|
||||
#include "gst.h"
|
||||
|
||||
*/
|
||||
import "C"
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"sync"
|
||||
"unsafe"
|
||||
|
||||
"github.com/pion/webrtc/v2"
|
||||
"github.com/pion/webrtc/v2/pkg/media"
|
||||
)
|
||||
|
||||
func init() {
|
||||
go C.gstreamer_send_start_mainloop()
|
||||
}
|
||||
|
||||
// Pipeline is a wrapper for a GStreamer Pipeline
|
||||
type Pipeline struct {
|
||||
Pipeline *C.GstElement
|
||||
tracks []*webrtc.Track
|
||||
id int
|
||||
codecName string
|
||||
clockRate float32
|
||||
}
|
||||
|
||||
var pipelines = make(map[int]*Pipeline)
|
||||
var pipelinesLock sync.Mutex
|
||||
|
||||
const (
|
||||
videoClockRate = 90000
|
||||
audioClockRate = 48000
|
||||
pcmClockRate = 8000
|
||||
)
|
||||
|
||||
// CreatePipeline creates a GStreamer Pipeline
|
||||
func CreatePipeline(codecName string, tracks []*webrtc.Track, pipelineSrc string) *Pipeline {
|
||||
pipelineStr := "appsink name=appsink"
|
||||
var clockRate float32
|
||||
|
||||
switch codecName {
|
||||
case webrtc.VP8:
|
||||
pipelineStr = pipelineSrc + " ! vp8enc error-resilient=partitions keyframe-max-dist=10 auto-alt-ref=true cpu-used=5 deadline=1 ! " + pipelineStr
|
||||
clockRate = videoClockRate
|
||||
|
||||
case webrtc.VP9:
|
||||
pipelineStr = pipelineSrc + " ! vp9enc ! " + pipelineStr
|
||||
clockRate = videoClockRate
|
||||
|
||||
case webrtc.H264:
|
||||
pipelineStr = pipelineSrc + " ! video/x-raw,format=I420 ! x264enc bframes=0 speed-preset=veryfast key-int-max=60 ! video/x-h264,stream-format=byte-stream ! " + pipelineStr
|
||||
clockRate = videoClockRate
|
||||
|
||||
case webrtc.Opus:
|
||||
pipelineStr = pipelineSrc + " ! opusenc ! " + pipelineStr
|
||||
clockRate = audioClockRate
|
||||
|
||||
case webrtc.G722:
|
||||
pipelineStr = pipelineSrc + " ! avenc_g722 ! " + pipelineStr
|
||||
clockRate = audioClockRate
|
||||
|
||||
case webrtc.PCMU:
|
||||
pipelineStr = pipelineSrc + " ! audio/x-raw, rate=8000 ! mulawenc ! " + pipelineStr
|
||||
clockRate = pcmClockRate
|
||||
|
||||
case webrtc.PCMA:
|
||||
pipelineStr = pipelineSrc + " ! audio/x-raw, rate=8000 ! alawenc ! " + pipelineStr
|
||||
clockRate = pcmClockRate
|
||||
|
||||
default:
|
||||
panic("Unhandled codec " + codecName)
|
||||
}
|
||||
|
||||
pipelineStrUnsafe := C.CString(pipelineStr)
|
||||
defer C.free(unsafe.Pointer(pipelineStrUnsafe))
|
||||
|
||||
pipelinesLock.Lock()
|
||||
defer pipelinesLock.Unlock()
|
||||
|
||||
pipeline := &Pipeline{
|
||||
Pipeline: C.gstreamer_send_create_pipeline(pipelineStrUnsafe),
|
||||
tracks: tracks,
|
||||
id: len(pipelines),
|
||||
codecName: codecName,
|
||||
clockRate: clockRate,
|
||||
}
|
||||
|
||||
pipelines[pipeline.id] = pipeline
|
||||
return pipeline
|
||||
}
|
||||
|
||||
// Start starts the GStreamer Pipeline
|
||||
func (p *Pipeline) Start() {
|
||||
C.gstreamer_send_start_pipeline(p.Pipeline, C.int(p.id))
|
||||
}
|
||||
|
||||
// Stop stops the GStreamer Pipeline
|
||||
func (p *Pipeline) Stop() {
|
||||
C.gstreamer_send_stop_pipeline(p.Pipeline)
|
||||
}
|
||||
|
||||
//export goHandlePipelineBuffer
|
||||
func goHandlePipelineBuffer(buffer unsafe.Pointer, bufferLen C.int, duration C.int, pipelineID C.int) {
|
||||
pipelinesLock.Lock()
|
||||
pipeline, ok := pipelines[int(pipelineID)]
|
||||
pipelinesLock.Unlock()
|
||||
|
||||
if ok {
|
||||
samples := uint32(pipeline.clockRate * (float32(duration) / 1000000000))
|
||||
for _, t := range pipeline.tracks {
|
||||
if err := t.WriteSample(media.Sample{Data: C.GoBytes(buffer, bufferLen), Samples: samples}); err != nil && err != io.ErrClosedPipe {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
fmt.Printf("discarding buffer, no pipeline with id %d", int(pipelineID))
|
||||
}
|
||||
C.free(buffer)
|
||||
}
|
16
server/internal/gst/gst.h
Normal file
16
server/internal/gst/gst.h
Normal file
@ -0,0 +1,16 @@
|
||||
#ifndef GST_H
|
||||
#define GST_H
|
||||
|
||||
#include <glib.h>
|
||||
#include <gst/gst.h>
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
extern void goHandlePipelineBuffer(void *buffer, int bufferLen, int samples, int pipelineId);
|
||||
|
||||
GstElement *gstreamer_send_create_pipeline(char *pipeline);
|
||||
void gstreamer_send_start_pipeline(GstElement *pipeline, int pipelineId);
|
||||
void gstreamer_send_stop_pipeline(GstElement *pipeline);
|
||||
void gstreamer_send_start_mainloop(void);
|
||||
|
||||
#endif
|
14
server/internal/http/api.go
Normal file
14
server/internal/http/api.go
Normal file
@ -0,0 +1,14 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"n.eko.moe/neko/internal/http/handler"
|
||||
)
|
||||
|
||||
func New(bind, password, static string) *http.Server {
|
||||
return &http.Server{
|
||||
Addr: bind,
|
||||
Handler: handler.New(password, static),
|
||||
}
|
||||
}
|
102
server/internal/http/endpoint/endpoint.go
Normal file
102
server/internal/http/endpoint/endpoint.go
Normal file
@ -0,0 +1,102 @@
|
||||
package endpoint
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"runtime/debug"
|
||||
|
||||
"github.com/go-chi/chi/middleware"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type (
|
||||
Endpoint func(http.ResponseWriter, *http.Request) error
|
||||
|
||||
ErrResponse struct {
|
||||
Status int `json:"status,omitempty"`
|
||||
Err string `json:"error,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Details string `json:"details,omitempty"`
|
||||
Code string `json:"code,omitempty"`
|
||||
RequestID string `json:"request,omitempty"`
|
||||
}
|
||||
)
|
||||
|
||||
func Handle(handler Endpoint) http.HandlerFunc {
|
||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||
if err := handler(w, r); err != nil {
|
||||
WriteError(w, r, err)
|
||||
}
|
||||
}
|
||||
|
||||
return http.HandlerFunc(fn)
|
||||
}
|
||||
|
||||
var nonErrorsCodes = map[int]bool{
|
||||
404: true,
|
||||
}
|
||||
|
||||
func errResponse(input interface{}) *ErrResponse {
|
||||
var res *ErrResponse
|
||||
var err interface{}
|
||||
|
||||
switch input.(type) {
|
||||
case *HandlerError:
|
||||
e := input.(*HandlerError)
|
||||
res = &ErrResponse{
|
||||
Status: e.Status,
|
||||
Err: http.StatusText(e.Status),
|
||||
Message: e.Message,
|
||||
}
|
||||
err = e.Err
|
||||
default:
|
||||
res = &ErrResponse{
|
||||
Status: http.StatusInternalServerError,
|
||||
Err: http.StatusText(http.StatusInternalServerError),
|
||||
}
|
||||
err = input
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
switch err.(type) {
|
||||
case *error:
|
||||
e := err.(error)
|
||||
res.Details = e.Error()
|
||||
break
|
||||
default:
|
||||
res.Details = fmt.Sprintf("%+v", err)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
func WriteError(w http.ResponseWriter, r *http.Request, err interface{}) {
|
||||
hlog := log.With().
|
||||
Str("module", "http").
|
||||
Logger()
|
||||
|
||||
res := errResponse(err)
|
||||
|
||||
if reqID := middleware.GetReqID(r.Context()); reqID != "" {
|
||||
res.RequestID = reqID
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(res.Status)
|
||||
|
||||
if err := json.NewEncoder(w).Encode(res); err != nil {
|
||||
hlog.Warn().Err(err).Msg("Failed writing json error response")
|
||||
}
|
||||
|
||||
if !nonErrorsCodes[res.Status] {
|
||||
logEntry := middleware.GetLogEntry(r)
|
||||
if logEntry != nil {
|
||||
logEntry.Panic(err, debug.Stack())
|
||||
} else {
|
||||
hlog.Error().Str("stack", string(debug.Stack())).Msgf("%+v", err)
|
||||
}
|
||||
}
|
||||
}
|
17
server/internal/http/endpoint/error.go
Normal file
17
server/internal/http/endpoint/error.go
Normal file
@ -0,0 +1,17 @@
|
||||
package endpoint
|
||||
|
||||
import "fmt"
|
||||
|
||||
type HandlerError struct {
|
||||
Status int
|
||||
Message string
|
||||
Err error
|
||||
}
|
||||
|
||||
func (e *HandlerError) Error() string {
|
||||
if e.Err != nil {
|
||||
return fmt.Sprintf("%s: %s", e.Message, e.Err.Error())
|
||||
}
|
||||
|
||||
return e.Message
|
||||
}
|
55
server/internal/http/handler/handler.go
Normal file
55
server/internal/http/handler/handler.go
Normal file
@ -0,0 +1,55 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"n.eko.moe/neko/internal/http/middleware"
|
||||
"n.eko.moe/neko/internal/http/endpoint"
|
||||
"n.eko.moe/neko/internal/webrtc"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
router *chi.Mux
|
||||
manager *webrtc.WebRTCManager
|
||||
}
|
||||
|
||||
func New(password, static string) *chi.Mux {
|
||||
router := chi.NewRouter()
|
||||
manager, err := webrtc.NewManager(password)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
handler := &Handler{
|
||||
router: router,
|
||||
manager: manager,
|
||||
}
|
||||
|
||||
router.Use(middleware.Recoverer) // Recover from panics without crashing server
|
||||
// router.Use(middleware.Logger) // Log API request calls
|
||||
|
||||
router.Get("/ping", endpoint.Handle(handler.Ping))
|
||||
router.Get("/ws", endpoint.Handle(handler.WebSocket))
|
||||
|
||||
fs := http.FileServer(http.Dir(static))
|
||||
router.Get("/*", func(w http.ResponseWriter, r *http.Request) {
|
||||
if _, err := os.Stat(static + r.RequestURI); os.IsNotExist(err) {
|
||||
http.StripPrefix(r.RequestURI, fs).ServeHTTP(w, r)
|
||||
} else {
|
||||
fs.ServeHTTP(w, r)
|
||||
}
|
||||
})
|
||||
|
||||
router.NotFound(endpoint.Handle(func(w http.ResponseWriter, r *http.Request) error {
|
||||
return &endpoint.HandlerError{
|
||||
Status: http.StatusNotFound,
|
||||
Message: fmt.Sprintf("Endpoint '%s' is not avalible", r.RequestURI),
|
||||
}
|
||||
}))
|
||||
|
||||
return router
|
||||
}
|
10
server/internal/http/handler/ping.go
Normal file
10
server/internal/http/handler/ping.go
Normal file
@ -0,0 +1,10 @@
|
||||
package handler
|
||||
|
||||
import "net/http"
|
||||
|
||||
func (h *Handler) Ping(w http.ResponseWriter, r *http.Request) error {
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("."))
|
||||
return nil
|
||||
}
|
9
server/internal/http/handler/websocket.go
Normal file
9
server/internal/http/handler/websocket.go
Normal file
@ -0,0 +1,9 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func (h *Handler) WebSocket(w http.ResponseWriter, r *http.Request) error {
|
||||
return h.manager.Upgrade(w, r)
|
||||
}
|
80
server/internal/http/middleware/logger.go
Normal file
80
server/internal/http/middleware/logger.go
Normal file
@ -0,0 +1,80 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/middleware"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func Logger(next http.Handler) http.Handler {
|
||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||
req := map[string]interface{}{}
|
||||
|
||||
if reqID := middleware.GetReqID(r.Context()); reqID != "" {
|
||||
req["id"] = reqID
|
||||
}
|
||||
|
||||
scheme := "http"
|
||||
if r.TLS != nil {
|
||||
scheme = "https"
|
||||
}
|
||||
|
||||
req["scheme"] = scheme
|
||||
req["proto"] = r.Proto
|
||||
req["method"] = r.Method
|
||||
req["remote"] = r.RemoteAddr
|
||||
req["agent"] = r.UserAgent()
|
||||
req["uri"] = fmt.Sprintf("%s://%s%s", scheme, r.Host, r.RequestURI)
|
||||
|
||||
fields := map[string]interface{}{}
|
||||
fields["req"] = req
|
||||
|
||||
entry := &entry{
|
||||
fields: fields,
|
||||
}
|
||||
|
||||
ww := middleware.NewWrapResponseWriter(w, r.ProtoMajor)
|
||||
t1 := time.Now()
|
||||
|
||||
defer func() {
|
||||
entry.Write(ww.Status(), ww.BytesWritten(), time.Since(t1))
|
||||
}()
|
||||
|
||||
next.ServeHTTP(ww, r)
|
||||
}
|
||||
return http.HandlerFunc(fn)
|
||||
}
|
||||
|
||||
type entry struct {
|
||||
fields map[string]interface{}
|
||||
errors []map[string]interface{}
|
||||
}
|
||||
|
||||
func (e *entry) Write(status, bytes int, elapsed time.Duration) {
|
||||
res := map[string]interface{}{}
|
||||
res["time"] = time.Now().UTC().Format(time.RFC1123)
|
||||
res["status"] = status
|
||||
res["bytes"] = bytes
|
||||
res["elapsed"] = float64(elapsed.Nanoseconds()) / 1000000.0
|
||||
|
||||
e.fields["res"] = res
|
||||
e.fields["module"] = "api"
|
||||
|
||||
if len(e.errors) > 0 {
|
||||
e.fields["errors"] = e.errors
|
||||
log.Error().Fields(e.fields).Msgf("Request failed (%d)", status)
|
||||
} else {
|
||||
log.Debug().Fields(e.fields).Msgf("Request complete (%d)", status)
|
||||
}
|
||||
}
|
||||
|
||||
func (e *entry) Panic(v interface{}, stack []byte) {
|
||||
err := map[string]interface{}{}
|
||||
err["message"] = fmt.Sprintf("%+v", v)
|
||||
err["stack"] = string(stack)
|
||||
|
||||
e.errors = append(e.errors, err)
|
||||
}
|
12
server/internal/http/middleware/middleware.go
Normal file
12
server/internal/http/middleware/middleware.go
Normal file
@ -0,0 +1,12 @@
|
||||
package middleware
|
||||
|
||||
// contextKey is a value for use with context.WithValue. It's used as
|
||||
// a pointer so it fits in an interface{} without allocation. This technique
|
||||
// for defining context keys was copied from Go 1.7's new use of context in net/http.
|
||||
type ctxKey struct {
|
||||
name string
|
||||
}
|
||||
|
||||
func (k *ctxKey) String() string {
|
||||
return "neko/ctx/" + k.name
|
||||
}
|
24
server/internal/http/middleware/recover.go
Normal file
24
server/internal/http/middleware/recover.go
Normal file
@ -0,0 +1,24 @@
|
||||
package middleware
|
||||
|
||||
// The original work was derived from Goji's middleware, source:
|
||||
// https://github.com/zenazn/goji/tree/master/web/middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"n.eko.moe/neko/internal/http/endpoint"
|
||||
)
|
||||
|
||||
func Recoverer(next http.Handler) http.Handler {
|
||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||
defer func() {
|
||||
if rvr := recover(); rvr != nil {
|
||||
endpoint.WriteError(w, r, rvr)
|
||||
}
|
||||
}()
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
return http.HandlerFunc(fn)
|
||||
}
|
89
server/internal/http/middleware/request.go
Normal file
89
server/internal/http/middleware/request.go
Normal file
@ -0,0 +1,89 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
// Key to use when setting the request ID.
|
||||
type ctxKeyRequestID int
|
||||
|
||||
// RequestIDKey is the key that holds the unique request ID in a request context.
|
||||
const RequestIDKey ctxKeyRequestID = 0
|
||||
|
||||
var prefix string
|
||||
var reqid uint64
|
||||
|
||||
// A quick note on the statistics here: we're trying to calculate the chance that
|
||||
// two randomly generated base62 prefixes will collide. We use the formula from
|
||||
// http://en.wikipedia.org/wiki/Birthday_problem
|
||||
//
|
||||
// P[m, n] \approx 1 - e^{-m^2/2n}
|
||||
//
|
||||
// We ballpark an upper bound for $m$ by imagining (for whatever reason) a server
|
||||
// that restarts every second over 10 years, for $m = 86400 * 365 * 10 = 315360000$
|
||||
//
|
||||
// For a $k$ character base-62 identifier, we have $n(k) = 62^k$
|
||||
//
|
||||
// Plugging this in, we find $P[m, n(10)] \approx 5.75%$, which is good enough for
|
||||
// our purposes, and is surely more than anyone would ever need in practice -- a
|
||||
// process that is rebooted a handful of times a day for a hundred years has less
|
||||
// than a millionth of a percent chance of generating two colliding IDs.
|
||||
|
||||
func init() {
|
||||
hostname, err := os.Hostname()
|
||||
if hostname == "" || err != nil {
|
||||
hostname = "localhost"
|
||||
}
|
||||
var buf [12]byte
|
||||
var b64 string
|
||||
for len(b64) < 10 {
|
||||
rand.Read(buf[:])
|
||||
b64 = base64.StdEncoding.EncodeToString(buf[:])
|
||||
b64 = strings.NewReplacer("+", "", "/", "").Replace(b64)
|
||||
}
|
||||
|
||||
prefix = fmt.Sprintf("%s/%s", hostname, b64[0:10])
|
||||
}
|
||||
|
||||
// RequestID is a middleware that injects a request ID into the context of each
|
||||
// request. A request ID is a string of the form "host.example.com/random-0001",
|
||||
// where "random" is a base62 random string that uniquely identifies this go
|
||||
// process, and where the last number is an atomically incremented request
|
||||
// counter.
|
||||
func RequestID(next http.Handler) http.Handler {
|
||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
requestID := r.Header.Get("X-Request-Id")
|
||||
if requestID == "" {
|
||||
myid := atomic.AddUint64(&reqid, 1)
|
||||
requestID = fmt.Sprintf("%s-%06d", prefix, myid)
|
||||
}
|
||||
ctx = context.WithValue(ctx, RequestIDKey, requestID)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
}
|
||||
return http.HandlerFunc(fn)
|
||||
}
|
||||
|
||||
// GetReqID returns a request ID from the given context if one is present.
|
||||
// Returns the empty string if a request ID cannot be found.
|
||||
func GetReqID(ctx context.Context) string {
|
||||
if ctx == nil {
|
||||
return ""
|
||||
}
|
||||
if reqID, ok := ctx.Value(RequestIDKey).(string); ok {
|
||||
return reqID
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// NextRequestID generates the next request ID in the sequence.
|
||||
func NextRequestID() uint64 {
|
||||
return atomic.AddUint64(&reqid, 1)
|
||||
}
|
32
server/internal/http/response/response.go
Normal file
32
server/internal/http/response/response.go
Normal file
@ -0,0 +1,32 @@
|
||||
package response
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"n.eko.moe/neko/internal/http/endpoint"
|
||||
)
|
||||
|
||||
// JSON encodes data to rw in JSON format. Returns a pointer to a
|
||||
// HandlerError if encoding fails.
|
||||
func JSON(w http.ResponseWriter, data interface{}, status int) error {
|
||||
w.WriteHeader(status)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
err := json.NewEncoder(w).Encode(data)
|
||||
if err != nil {
|
||||
return &endpoint.HandlerError{
|
||||
Status: http.StatusInternalServerError,
|
||||
Message: "Unable to write JSON response",
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Empty merely sets the response code to NoContent (204).
|
||||
func Empty(w http.ResponseWriter) error {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
return nil
|
||||
}
|
203
server/internal/keys/keyboard.go
Normal file
203
server/internal/keys/keyboard.go
Normal file
@ -0,0 +1,203 @@
|
||||
package keys
|
||||
|
||||
const KEY_0 = 48
|
||||
const KEY_1 = 49
|
||||
const KEY_2 = 50
|
||||
const KEY_3 = 51
|
||||
const KEY_4 = 52
|
||||
const KEY_5 = 53
|
||||
const KEY_6 = 54
|
||||
const KEY_7 = 55
|
||||
const KEY_8 = 56
|
||||
const KEY_9 = 57
|
||||
|
||||
const KEY_A = 65
|
||||
const KEY_B = 66
|
||||
const KEY_C = 67
|
||||
const KEY_D = 68
|
||||
const KEY_E = 69
|
||||
const KEY_F = 70
|
||||
const KEY_G = 71
|
||||
const KEY_H = 72
|
||||
const KEY_I = 73
|
||||
const KEY_J = 74
|
||||
const KEY_K = 75
|
||||
const KEY_L = 76
|
||||
const KEY_M = 77
|
||||
const KEY_N = 78
|
||||
const KEY_O = 79
|
||||
const KEY_P = 80
|
||||
const KEY_Q = 81
|
||||
const KEY_R = 82
|
||||
const KEY_S = 83
|
||||
const KEY_T = 84
|
||||
const KEY_U = 85
|
||||
const KEY_V = 86
|
||||
const KEY_W = 87
|
||||
const KEY_X = 88
|
||||
const KEY_Y = 89
|
||||
const KEY_Z = 90
|
||||
|
||||
const KEY_NUMPAD0 = 96
|
||||
const KEY_NUMPAD1 = 97
|
||||
const KEY_NUMPAD2 = 98
|
||||
const KEY_NUMPAD3 = 99
|
||||
const KEY_NUMPAD4 = 100
|
||||
const KEY_NUMPAD5 = 101
|
||||
const KEY_NUMPAD6 = 102
|
||||
const KEY_NUMPAD7 = 103
|
||||
const KEY_NUMPAD8 = 104
|
||||
const KEY_NUMPAD9 = 105
|
||||
|
||||
const KEY_F1 = 112
|
||||
const KEY_F2 = 113
|
||||
const KEY_F3 = 114
|
||||
const KEY_F4 = 115
|
||||
const KEY_F5 = 116
|
||||
const KEY_F6 = 117
|
||||
const KEY_F7 = 118
|
||||
const KEY_F8 = 119
|
||||
const KEY_F9 = 120
|
||||
const KEY_F10 = 121
|
||||
const KEY_F11 = 122
|
||||
const KEY_F12 = 123
|
||||
|
||||
const KEY_BACK_SPACE = 8
|
||||
const KEY_TAB = 9
|
||||
const KEY_ENTER = 13
|
||||
const KEY_ENTER_ALT = 14
|
||||
const KEY_SHIFT = 16
|
||||
const KEY_CONTROL = 17
|
||||
const KEY_ALT = 18
|
||||
const KEY_ESCAPE = 27
|
||||
const KEY_SPACE = 32
|
||||
const KEY_PAGE_UP = 33
|
||||
const KEY_PAGE_DOWN = 34
|
||||
const KEY_END = 35
|
||||
const KEY_LEFT = 37
|
||||
const KEY_UP = 38
|
||||
const KEY_RIGHT = 39
|
||||
const KEY_DOWN = 40
|
||||
const KEY_DELETE = 46
|
||||
const KEY_SEMICOLON = 59
|
||||
const KEY_SEMICOLON_ALT = 186
|
||||
const KEY_EQUALS = 61
|
||||
const KEY_EQUALS_ALT = 187
|
||||
const KEY_MULTIPLY = 106
|
||||
const KEY_ADD = 107
|
||||
const KEY_SEPARATOR = 108
|
||||
const KEY_SUBTRACT = 109
|
||||
const KEY_SUBTRACT_ALT = 189
|
||||
const KEY_DECIMAL = 110
|
||||
const KEY_DIVIDE = 111
|
||||
const KEY_COMMA = 188
|
||||
const KEY_PERIOD = 190
|
||||
const KEY_SLASH = 191
|
||||
const KEY_BACK_QUOTE = 192
|
||||
const KEY_BACK_SLASH = 220
|
||||
const KEY_OPEN_BRACKET = 219
|
||||
const KEY_CLOSE_BRACKET = 221
|
||||
const KEY_QUOTE = 222
|
||||
|
||||
var Keyboard = map[int]string{}
|
||||
|
||||
func init() {
|
||||
Keyboard[KEY_A] = "a"
|
||||
Keyboard[KEY_B] = "b"
|
||||
Keyboard[KEY_C] = "c"
|
||||
Keyboard[KEY_D] = "d"
|
||||
Keyboard[KEY_E] = "e"
|
||||
Keyboard[KEY_F] = "f"
|
||||
Keyboard[KEY_G] = "g"
|
||||
Keyboard[KEY_H] = "h"
|
||||
Keyboard[KEY_I] = "i"
|
||||
Keyboard[KEY_J] = "j"
|
||||
Keyboard[KEY_K] = "k"
|
||||
Keyboard[KEY_L] = "l"
|
||||
Keyboard[KEY_M] = "m"
|
||||
Keyboard[KEY_N] = "n"
|
||||
Keyboard[KEY_O] = "o"
|
||||
Keyboard[KEY_P] = "p"
|
||||
Keyboard[KEY_Q] = "q"
|
||||
Keyboard[KEY_R] = "r"
|
||||
Keyboard[KEY_S] = "s"
|
||||
Keyboard[KEY_T] = "r"
|
||||
Keyboard[KEY_U] = "u"
|
||||
Keyboard[KEY_V] = "v"
|
||||
Keyboard[KEY_W] = "w"
|
||||
Keyboard[KEY_X] = "x"
|
||||
Keyboard[KEY_Y] = "y"
|
||||
Keyboard[KEY_Z] = "z"
|
||||
|
||||
Keyboard[KEY_0] = "0"
|
||||
Keyboard[KEY_1] = "1"
|
||||
Keyboard[KEY_2] = "2"
|
||||
Keyboard[KEY_3] = "3"
|
||||
Keyboard[KEY_4] = "4"
|
||||
Keyboard[KEY_5] = "5"
|
||||
Keyboard[KEY_6] = "6"
|
||||
Keyboard[KEY_7] = "7"
|
||||
Keyboard[KEY_8] = "8"
|
||||
Keyboard[KEY_9] = "9"
|
||||
|
||||
Keyboard[KEY_NUMPAD0] = "0"
|
||||
Keyboard[KEY_NUMPAD1] = "1"
|
||||
Keyboard[KEY_NUMPAD2] = "2"
|
||||
Keyboard[KEY_NUMPAD3] = "3"
|
||||
Keyboard[KEY_NUMPAD4] = "4"
|
||||
Keyboard[KEY_NUMPAD5] = "5"
|
||||
Keyboard[KEY_NUMPAD6] = "6"
|
||||
Keyboard[KEY_NUMPAD7] = "7"
|
||||
Keyboard[KEY_NUMPAD8] = "8"
|
||||
Keyboard[KEY_NUMPAD9] = "9"
|
||||
|
||||
Keyboard[KEY_F1] = "f1"
|
||||
Keyboard[KEY_F2] = "f2"
|
||||
Keyboard[KEY_F3] = "f3"
|
||||
Keyboard[KEY_F4] = "f4"
|
||||
Keyboard[KEY_F5] = "f5"
|
||||
Keyboard[KEY_F6] = "f6"
|
||||
Keyboard[KEY_F7] = "f7"
|
||||
Keyboard[KEY_F8] = "f8"
|
||||
Keyboard[KEY_F9] = "f9"
|
||||
Keyboard[KEY_F10] = "f10"
|
||||
Keyboard[KEY_F11] = "f11"
|
||||
Keyboard[KEY_F12] = "f12"
|
||||
|
||||
Keyboard[KEY_QUOTE] = "'"
|
||||
Keyboard[KEY_COMMA] = ","
|
||||
Keyboard[KEY_PERIOD] = "."
|
||||
Keyboard[KEY_SEMICOLON] = ";"
|
||||
Keyboard[KEY_SEMICOLON_ALT] = ";"
|
||||
Keyboard[KEY_SLASH] = "/"
|
||||
Keyboard[KEY_BACK_SLASH] = "\\"
|
||||
Keyboard[KEY_BACK_QUOTE] = "`"
|
||||
Keyboard[KEY_OPEN_BRACKET] = "["
|
||||
Keyboard[KEY_CLOSE_BRACKET] = "]"
|
||||
Keyboard[KEY_EQUALS] = "="
|
||||
Keyboard[KEY_EQUALS_ALT] = "="
|
||||
Keyboard[KEY_MULTIPLY] = "*"
|
||||
Keyboard[KEY_ADD] = "+"
|
||||
Keyboard[KEY_SEPARATOR] = "."
|
||||
Keyboard[KEY_SUBTRACT] = "-"
|
||||
Keyboard[KEY_SUBTRACT_ALT] = "-"
|
||||
Keyboard[KEY_DECIMAL] = "."
|
||||
Keyboard[KEY_DIVIDE] = "/"
|
||||
Keyboard[KEY_BACK_SPACE] = "backspace"
|
||||
Keyboard[KEY_DELETE] = "delete"
|
||||
Keyboard[KEY_ENTER] = "enter"
|
||||
Keyboard[KEY_ENTER_ALT] = "enter"
|
||||
Keyboard[KEY_TAB] = "tab"
|
||||
Keyboard[KEY_ESCAPE] = "escape"
|
||||
Keyboard[KEY_UP] = "up"
|
||||
Keyboard[KEY_DOWN] = "down"
|
||||
Keyboard[KEY_RIGHT] = "right"
|
||||
Keyboard[KEY_LEFT] = "left"
|
||||
Keyboard[KEY_END] = "end"
|
||||
Keyboard[KEY_PAGE_UP] = "pageup"
|
||||
Keyboard[KEY_PAGE_DOWN] = "pagedown"
|
||||
Keyboard[KEY_ALT] = "alt"
|
||||
Keyboard[KEY_CONTROL] = "control"
|
||||
Keyboard[KEY_SHIFT] = "shift"
|
||||
Keyboard[KEY_SPACE] = "space"
|
||||
}
|
21
server/internal/keys/mouse.go
Normal file
21
server/internal/keys/mouse.go
Normal file
@ -0,0 +1,21 @@
|
||||
package keys
|
||||
|
||||
const MOUSE_LEFT = 0
|
||||
const MOUSE_MIDDLE = 1
|
||||
const MOUSE_RIGHT = 2
|
||||
const MOUSE_WHEEL_UP = 4
|
||||
const MOUSE_WHEEL_DOWN = 5
|
||||
const MOUSE_WHEEL_RIGH = 6
|
||||
const MOUSE_WHEEL_LEFT = 7
|
||||
|
||||
var Mouse = map[int]string{}
|
||||
|
||||
func init() {
|
||||
Mouse[MOUSE_LEFT] = "left"
|
||||
Mouse[MOUSE_MIDDLE] = "center"
|
||||
Mouse[MOUSE_RIGHT] = "right"
|
||||
Mouse[MOUSE_WHEEL_UP] = "wheelUp"
|
||||
Mouse[MOUSE_WHEEL_DOWN] = "wheelDown"
|
||||
Mouse[MOUSE_WHEEL_RIGH] = "wheelRight"
|
||||
Mouse[MOUSE_WHEEL_LEFT] = "wheelLeft"
|
||||
}
|
71
server/internal/nanoid/nanoid.go
Normal file
71
server/internal/nanoid/nanoid.go
Normal file
@ -0,0 +1,71 @@
|
||||
package nanoid
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
gonanoid "github.com/matoous/go-nanoid"
|
||||
)
|
||||
|
||||
var nano *NanoID
|
||||
|
||||
func init() {
|
||||
nano = &NanoID{
|
||||
alphabet: "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ",
|
||||
size: 16,
|
||||
}
|
||||
}
|
||||
|
||||
func New(alphabet string, size int) *NanoID {
|
||||
return &NanoID{
|
||||
alphabet: alphabet,
|
||||
size: size,
|
||||
}
|
||||
}
|
||||
|
||||
type NanoID struct {
|
||||
alphabet string
|
||||
size int
|
||||
}
|
||||
|
||||
func (n *NanoID) NewID() (string, error) {
|
||||
return gonanoid.Generate(n.alphabet, n.size)
|
||||
}
|
||||
|
||||
func (n *NanoID) NewIDSize(size int) (string, error) {
|
||||
return gonanoid.Generate(n.alphabet, size)
|
||||
}
|
||||
|
||||
func (n *NanoID) NewIDRang(max int, min int) (string, error) {
|
||||
rand.Seed(time.Now().Unix())
|
||||
return gonanoid.Generate(n.alphabet, rand.Intn(max-min)+min)
|
||||
}
|
||||
|
||||
func (n *NanoID) GenerateID(alphabet string, size int) (string, error) {
|
||||
return gonanoid.Generate(alphabet, size)
|
||||
}
|
||||
|
||||
func (n *NanoID) GenerateIDRange(alphabet string, max int, min int) (string, error) {
|
||||
rand.Seed(time.Now().Unix())
|
||||
return gonanoid.Generate(alphabet, rand.Intn(max-min)+min)
|
||||
}
|
||||
|
||||
func NewID() (string, error) {
|
||||
return nano.NewID()
|
||||
}
|
||||
|
||||
func NewIDSize(size int) (string, error) {
|
||||
return nano.NewIDSize(size)
|
||||
}
|
||||
|
||||
func NewIDRang(max int, min int) (string, error) {
|
||||
return nano.NewIDRang(max, min)
|
||||
}
|
||||
|
||||
func GenerateID(alphabet string, size int) (string, error) {
|
||||
return nano.GenerateID(alphabet, size)
|
||||
}
|
||||
|
||||
func GenerateIDRange(alphabet string, max int, min int) (string, error) {
|
||||
return nano.GenerateIDRange(alphabet, max, min)
|
||||
}
|
48
server/internal/preflight/config.go
Normal file
48
server/internal/preflight/config.go
Normal file
@ -0,0 +1,48 @@
|
||||
package preflight
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
func Config(name string) {
|
||||
config := viper.GetString("neko.config")
|
||||
|
||||
if config != "" {
|
||||
viper.SetConfigFile(config) // Use config file from the flag.
|
||||
} else {
|
||||
if runtime.GOOS == "linux" {
|
||||
viper.AddConfigPath("/etc/neko/")
|
||||
}
|
||||
|
||||
viper.AddConfigPath(".")
|
||||
viper.SetConfigName(name)
|
||||
}
|
||||
|
||||
viper.SetEnvPrefix("NEKO")
|
||||
viper.AutomaticEnv() // read in environment variables that match
|
||||
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
|
||||
log.Error().Err(err)
|
||||
}
|
||||
if config != "" {
|
||||
log.Error().Err(err)
|
||||
}
|
||||
}
|
||||
|
||||
file := viper.ConfigFileUsed()
|
||||
logger := log.With().
|
||||
Bool("debug", viper.GetBool("neko.debug")).
|
||||
Str("logging", viper.GetString("neko.logs")).
|
||||
Str("config", file).
|
||||
Logger()
|
||||
|
||||
if file == "" {
|
||||
logger.Warn().Msg("Preflight complete without config file")
|
||||
} else {
|
||||
logger.Info().Msg("Preflight complete")
|
||||
}
|
||||
}
|
60
server/internal/preflight/logs.go
Normal file
60
server/internal/preflight/logs.go
Normal file
@ -0,0 +1,60 @@
|
||||
package preflight
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/diode"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
func Logs(name string) {
|
||||
zerolog.TimeFieldFormat = ""
|
||||
zerolog.SetGlobalLevel(zerolog.InfoLevel)
|
||||
|
||||
if viper.GetBool("neko.debug") {
|
||||
zerolog.SetGlobalLevel(zerolog.DebugLevel)
|
||||
}
|
||||
|
||||
console := zerolog.ConsoleWriter{Out: os.Stdout}
|
||||
|
||||
if !viper.GetBool("neko.logs") {
|
||||
log.Logger = log.Output(console)
|
||||
} else {
|
||||
|
||||
logs := filepath.Join(".", "logs")
|
||||
if runtime.GOOS == "linux" {
|
||||
logs = "/var/log/neko"
|
||||
}
|
||||
|
||||
if _, err := os.Stat(logs); os.IsNotExist(err) {
|
||||
os.Mkdir(logs, os.ModePerm)
|
||||
}
|
||||
|
||||
latest := filepath.Join(logs, name+"-latest.log")
|
||||
_, err := os.Stat(latest)
|
||||
if err == nil {
|
||||
err = os.Rename(latest, filepath.Join(logs, "neko."+time.Now().Format("2006-01-02T15-04-05Z07-00")+".log"))
|
||||
if err != nil {
|
||||
log.Panic().Err(err).Msg("Failed to rotate log file")
|
||||
}
|
||||
}
|
||||
|
||||
logf, err := os.OpenFile(latest, os.O_RDWR|os.O_CREATE, 0666)
|
||||
if err != nil {
|
||||
log.Panic().Err(err).Msg("Failed to create log file")
|
||||
}
|
||||
|
||||
logger := diode.NewWriter(logf, 1000, 10*time.Millisecond, func(missed int) {
|
||||
fmt.Printf("Logger Dropped %d messages", missed)
|
||||
})
|
||||
|
||||
log.Logger = log.Output(io.MultiWriter(console, logger))
|
||||
}
|
||||
}
|
21
server/internal/structs/version.go
Normal file
21
server/internal/structs/version.go
Normal file
@ -0,0 +1,21 @@
|
||||
package structs
|
||||
|
||||
import "fmt"
|
||||
|
||||
type Version struct {
|
||||
Major string
|
||||
Minor string
|
||||
Patch string
|
||||
Version string
|
||||
GitVersion string
|
||||
GitCommit string
|
||||
GitTreeState string
|
||||
BuildDate string
|
||||
GoVersion string
|
||||
Compiler string
|
||||
Platform string
|
||||
}
|
||||
|
||||
func (i *Version) String() string {
|
||||
return fmt.Sprintf("%s.%s.%s", i.Major, i.Minor, i.Patch)
|
||||
}
|
34
server/internal/utils/color.go
Normal file
34
server/internal/utils/color.go
Normal file
@ -0,0 +1,34 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
const (
|
||||
char = "&"
|
||||
)
|
||||
|
||||
// Colors: http://www.lihaoyi.com/post/BuildyourownCommandLinewithANSIescapecodes.html
|
||||
var re = regexp.MustCompile(char + `(?m)([0-9]{1,2};[0-9]{1,2}|[0-9]{1,2})`)
|
||||
|
||||
func Color(str string) string {
|
||||
result := ""
|
||||
lastIndex := 0
|
||||
|
||||
for _, v := range re.FindAllSubmatchIndex([]byte(str), -1) {
|
||||
groups := []string{}
|
||||
for i := 0; i < len(v); i += 2 {
|
||||
groups = append(groups, str[v[i]:v[i+1]])
|
||||
}
|
||||
|
||||
result += str[lastIndex:v[0]] + "\033[" + groups[1] + "m"
|
||||
lastIndex = v[1]
|
||||
}
|
||||
|
||||
return result + str[lastIndex:]
|
||||
}
|
||||
|
||||
func Colorf(format string, a ...interface{}) string {
|
||||
return fmt.Sprintf(Color(format), a...)
|
||||
}
|
10
server/internal/utils/header.go
Normal file
10
server/internal/utils/header.go
Normal file
@ -0,0 +1,10 @@
|
||||
package utils
|
||||
|
||||
const Header = `&34
|
||||
_ __ __
|
||||
/ | / /__ / /______ \ /\
|
||||
/ |/ / _ \/ //_/ __ \ ) ( ')
|
||||
/ /| / __/ ,< / /_/ / ( / )
|
||||
/_/ |_/\___/_/|_|\____/ \(__)|
|
||||
&1&37 nurdism/neko &33%s v%s&0
|
||||
`
|
25
server/internal/utils/map.go
Normal file
25
server/internal/utils/map.go
Normal file
@ -0,0 +1,25 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
type CountedSyncMap struct {
|
||||
sync.Map
|
||||
len uint64
|
||||
}
|
||||
|
||||
func (m *CountedSyncMap) CountedDelete(key interface{}) {
|
||||
m.Delete(key)
|
||||
atomic.AddUint64(&m.len, ^uint64(0))
|
||||
}
|
||||
|
||||
func (m *CountedSyncMap) CountedStore(key, value interface{}) {
|
||||
m.Store(key, value)
|
||||
atomic.AddUint64(&m.len, uint64(1))
|
||||
}
|
||||
|
||||
func (m *CountedSyncMap) CountedLen() uint64 {
|
||||
return atomic.LoadUint64(&m.len)
|
||||
}
|
22
server/internal/webrtc/data.go
Normal file
22
server/internal/webrtc/data.go
Normal file
@ -0,0 +1,22 @@
|
||||
package webrtc
|
||||
|
||||
type dataHeader struct {
|
||||
Event uint8
|
||||
Length uint16
|
||||
}
|
||||
|
||||
type dataMouseMove struct {
|
||||
dataHeader
|
||||
X int16
|
||||
Y int16
|
||||
}
|
||||
|
||||
type dataMouseKey struct {
|
||||
dataHeader
|
||||
Key uint8
|
||||
}
|
||||
|
||||
type dataKeyboardKey struct {
|
||||
dataHeader
|
||||
Key uint16
|
||||
}
|
73
server/internal/webrtc/manager.go
Normal file
73
server/internal/webrtc/manager.go
Normal file
@ -0,0 +1,73 @@
|
||||
package webrtc
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/pion/webrtc/v2"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"n.eko.moe/neko/internal/gst"
|
||||
)
|
||||
|
||||
func NewManager(password string) (*WebRTCManager, error) {
|
||||
engine := webrtc.MediaEngine{}
|
||||
|
||||
videoCodec := webrtc.NewRTPVP8Codec(webrtc.DefaultPayloadTypeVP8, 90000)
|
||||
video, err := webrtc.NewTrack(webrtc.DefaultPayloadTypeVP8, rand.Uint32(), "stream", "stream", videoCodec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
gst.CreatePipeline(webrtc.VP8, []*webrtc.Track{video}, "ximagesrc show-pointer=true use-damage=false ! video/x-raw,framerate=30/1 ! videoconvert").Start()
|
||||
engine.RegisterCodec(videoCodec)
|
||||
// ximagesrc xid=0 show-pointer=true ! videoconvert ! queue | videotestsrc
|
||||
|
||||
audioCodec := webrtc.NewRTPOpusCodec(webrtc.DefaultPayloadTypeOpus, 48000)
|
||||
audio, err := webrtc.NewTrack(webrtc.DefaultPayloadTypeOpus, rand.Uint32(), "stream", "stream", audioCodec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
gst.CreatePipeline(webrtc.Opus, []*webrtc.Track{audio}, "pulsesrc device=auto_null.monitor ! audioconvert").Start()
|
||||
engine.RegisterCodec(audioCodec)
|
||||
// pulsesrc device=auto_null.monitor ! audioconvert | audiotestsrc
|
||||
// gst-launch-1.0 -v pulsesrc device=auto_null.monitor ! audioconvert ! vorbisenc ! oggmux ! filesink location=alsasrc.ogg
|
||||
|
||||
return &WebRTCManager{
|
||||
logger: log.With().Str("service", "webrtc").Logger(),
|
||||
engine: engine,
|
||||
api: webrtc.NewAPI(webrtc.WithMediaEngine(engine)),
|
||||
video: video,
|
||||
audio: audio,
|
||||
controller: "",
|
||||
password: password,
|
||||
sessions: make(map[string]*session),
|
||||
upgrader: websocket.Upgrader{
|
||||
CheckOrigin: func(r *http.Request) bool {
|
||||
return true
|
||||
},
|
||||
},
|
||||
config: webrtc.Configuration{
|
||||
ICEServers: []webrtc.ICEServer{
|
||||
{
|
||||
URLs: []string{"stun:stun.l.google.com:19302"},
|
||||
},
|
||||
},
|
||||
SDPSemantics: webrtc.SDPSemanticsUnifiedPlanWithFallback,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
type WebRTCManager struct {
|
||||
logger zerolog.Logger
|
||||
upgrader websocket.Upgrader
|
||||
engine webrtc.MediaEngine
|
||||
api *webrtc.API
|
||||
config webrtc.Configuration
|
||||
password string
|
||||
controller string
|
||||
sessions map[string]*session
|
||||
video *webrtc.Track
|
||||
audio *webrtc.Track
|
||||
}
|
15
server/internal/webrtc/messages.go
Normal file
15
server/internal/webrtc/messages.go
Normal file
@ -0,0 +1,15 @@
|
||||
package webrtc
|
||||
|
||||
type message struct {
|
||||
Event string `json:"event"`
|
||||
}
|
||||
|
||||
type messageIdentityProvide struct {
|
||||
message
|
||||
ID string `json:"id"`
|
||||
}
|
||||
|
||||
type messageSDP struct {
|
||||
message
|
||||
SDP string `json:"sdp"`
|
||||
}
|
220
server/internal/webrtc/peer.go
Normal file
220
server/internal/webrtc/peer.go
Normal file
@ -0,0 +1,220 @@
|
||||
package webrtc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/go-vgo/robotgo"
|
||||
"github.com/pion/webrtc/v2"
|
||||
|
||||
"n.eko.moe/neko/internal/keys"
|
||||
)
|
||||
|
||||
func (manager *WebRTCManager) createPeer(session *session, raw []byte) error {
|
||||
payload := messageSDP{}
|
||||
if err := json.Unmarshal(raw, &payload); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
peer, err := manager.api.NewPeerConnection(manager.config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = peer.AddTrack(manager.video)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = peer.AddTrack(manager.audio)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
peer.SetRemoteDescription(webrtc.SessionDescription{
|
||||
SDP: payload.SDP,
|
||||
Type: webrtc.SDPTypeOffer,
|
||||
})
|
||||
|
||||
answer, err := peer.CreateAnswer(nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = peer.SetLocalDescription(answer); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
session.send(messageSDP{
|
||||
message{Event: "sdp/reply"},
|
||||
answer.SDP,
|
||||
})
|
||||
|
||||
session.peer = peer
|
||||
|
||||
peer.OnDataChannel(func(d *webrtc.DataChannel) {
|
||||
d.OnMessage(func(msg webrtc.DataChannelMessage) {
|
||||
if err = manager.onData(session, msg); err != nil {
|
||||
manager.logger.Warn().Err(err).Msg("onData failed")
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
peer.OnConnectionStateChange(func(connectionState webrtc.PeerConnectionState) {
|
||||
switch connectionState {
|
||||
case webrtc.PeerConnectionStateDisconnected:
|
||||
case webrtc.PeerConnectionStateFailed:
|
||||
manager.destroy(session)
|
||||
break
|
||||
case webrtc.PeerConnectionStateConnected:
|
||||
manager.logger.Info().Str("ID", session.id).Msg("Peer connected")
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var debounce = map[int]bool{}
|
||||
|
||||
func (manager *WebRTCManager) onData(session *session, msg webrtc.DataChannelMessage) error {
|
||||
if manager.controller != session.id {
|
||||
return nil
|
||||
}
|
||||
|
||||
header := &dataHeader{}
|
||||
buffer := bytes.NewBuffer(msg.Data)
|
||||
byt := make([]byte, 3)
|
||||
_, err := buffer.Read(byt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = binary.Read(bytes.NewBuffer(byt), binary.LittleEndian, header)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
buffer = bytes.NewBuffer(msg.Data)
|
||||
|
||||
switch header.Event {
|
||||
case 0x01: // MOUSE_MOVE
|
||||
payload := &dataMouseMove{}
|
||||
err := binary.Read(buffer, binary.LittleEndian, payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
robotgo.Move(int(payload.X), int(payload.Y))
|
||||
break
|
||||
case 0x02: // MOUSE_UP
|
||||
payload := &dataMouseKey{}
|
||||
err := binary.Read(buffer, binary.LittleEndian, payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if key, ok := keys.Mouse[int(payload.Key)]; ok {
|
||||
if !debounce[int(payload.Key)] {
|
||||
return nil
|
||||
}
|
||||
debounce[int(payload.Key)] = false
|
||||
robotgo.MouseToggle("up", key)
|
||||
} else {
|
||||
manager.logger.Warn().Msgf("Unknown MOUSE_DOWN key: %v", payload.Key)
|
||||
}
|
||||
break
|
||||
case 0x03: // MOUSE_DOWN
|
||||
payload := &dataMouseKey{}
|
||||
err := binary.Read(buffer, binary.LittleEndian, payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if key, ok := keys.Mouse[int(payload.Key)]; ok {
|
||||
if debounce[int(payload.Key)] {
|
||||
return nil
|
||||
}
|
||||
debounce[int(payload.Key)] = true
|
||||
|
||||
robotgo.MouseToggle("down", key)
|
||||
} else {
|
||||
manager.logger.Warn().Msgf("Unknown MOUSE_DOWN key: %v", payload.Key)
|
||||
}
|
||||
break
|
||||
case 0x04: // MOUSE_CLK
|
||||
payload := &dataMouseKey{}
|
||||
err := binary.Read(buffer, binary.LittleEndian, payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if key, ok := keys.Mouse[int(payload.Key)]; ok {
|
||||
switch int(payload.Key) {
|
||||
case keys.MOUSE_WHEEL_DOWN:
|
||||
robotgo.Scroll(0, -10)
|
||||
break
|
||||
case keys.MOUSE_WHEEL_UP:
|
||||
robotgo.Scroll(0, 10)
|
||||
break
|
||||
case keys.MOUSE_WHEEL_LEFT:
|
||||
robotgo.Scroll(-10, 0)
|
||||
break
|
||||
case keys.MOUSE_WHEEL_RIGH:
|
||||
robotgo.Scroll(10, 0)
|
||||
break
|
||||
default:
|
||||
robotgo.Click(key, false)
|
||||
}
|
||||
} else {
|
||||
manager.logger.Warn().Msgf("Unknown MOUSE_CLK key: %v", payload.Key)
|
||||
}
|
||||
break
|
||||
case 0x05: // KEY_DOWN
|
||||
payload := &dataKeyboardKey{}
|
||||
err := binary.Read(buffer, binary.LittleEndian, payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if key, ok := keys.Keyboard[int(payload.Key)]; ok {
|
||||
if debounce[int(payload.Key)] {
|
||||
return nil
|
||||
}
|
||||
debounce[int(payload.Key)] = true
|
||||
robotgo.KeyToggle(key, "down")
|
||||
} else {
|
||||
manager.logger.Warn().Msgf("Unknown KEY_DOWN key: %v", payload.Key)
|
||||
}
|
||||
break
|
||||
case 0x06: // KEY_UP
|
||||
payload := &dataKeyboardKey{}
|
||||
err := binary.Read(buffer, binary.LittleEndian, payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if key, ok := keys.Keyboard[int(payload.Key)]; ok {
|
||||
if !debounce[int(payload.Key)] {
|
||||
return nil
|
||||
}
|
||||
debounce[int(payload.Key)] = false
|
||||
robotgo.KeyToggle(key, "up")
|
||||
} else {
|
||||
manager.logger.Warn().Msgf("Unknown KEY_UP key: %v", payload.Key)
|
||||
}
|
||||
break
|
||||
case 0x07: // KEY_CLK
|
||||
payload := &dataKeyboardKey{}
|
||||
err := binary.Read(buffer, binary.LittleEndian, payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if key, ok := keys.Keyboard[int(payload.Key)]; ok {
|
||||
robotgo.KeyTap(key)
|
||||
} else {
|
||||
manager.logger.Warn().Msgf("Unknown KEY_CLK key: %v", payload.Key)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
41
server/internal/webrtc/session.go
Normal file
41
server/internal/webrtc/session.go
Normal file
@ -0,0 +1,41 @@
|
||||
package webrtc
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/pion/webrtc/v2"
|
||||
)
|
||||
|
||||
type session struct {
|
||||
id string
|
||||
socket *websocket.Conn
|
||||
peer *webrtc.PeerConnection
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func (session *session) send(v interface{}) error {
|
||||
session.mu.Lock()
|
||||
defer session.mu.Unlock()
|
||||
|
||||
if session.socket != nil {
|
||||
return session.socket.WriteJSON(v)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (session *session) destroy() error {
|
||||
if session.peer != nil && session.peer.ConnectionState() == webrtc.PeerConnectionStateConnected {
|
||||
if err := session.peer.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if session.socket != nil {
|
||||
if err := session.socket.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
222
server/internal/webrtc/websocket.go
Normal file
222
server/internal/webrtc/websocket.go
Normal file
@ -0,0 +1,222 @@
|
||||
package webrtc
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"n.eko.moe/neko/internal/nanoid"
|
||||
)
|
||||
|
||||
const (
|
||||
// Send pings to peer with this period. Must be less than pongWait.
|
||||
pingPeriod = 60 * time.Second
|
||||
)
|
||||
|
||||
func (manager *WebRTCManager) Upgrade(w http.ResponseWriter, r *http.Request) error {
|
||||
socket, err := manager.upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
manager.logger.Error().Err(err).Msg("Failed to upgrade websocket!")
|
||||
return nil
|
||||
}
|
||||
|
||||
sessionID, ok := manager.authenticate(r)
|
||||
if ok != true {
|
||||
manager.logger.Warn().Msg("Authenticatetion failed")
|
||||
if err = socket.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
session := &session{
|
||||
id: sessionID,
|
||||
socket: socket,
|
||||
mu: sync.Mutex{},
|
||||
}
|
||||
|
||||
manager.logger.
|
||||
Info().
|
||||
Str("ID", sessionID).
|
||||
Str("RemoteAddr", socket.RemoteAddr().String()).
|
||||
Msg("Created Session")
|
||||
|
||||
manager.sessions[sessionID] = session
|
||||
|
||||
defer func() {
|
||||
manager.destroy(session)
|
||||
}()
|
||||
|
||||
if err = manager.onConnected(session); err != nil {
|
||||
manager.logger.Error().Err(err).Msg("onConnected failed!")
|
||||
return nil
|
||||
}
|
||||
|
||||
manager.handleWS(session)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (manager *WebRTCManager) authenticate(r *http.Request) (sessionID string, ok bool) {
|
||||
|
||||
passwords, ok := r.URL.Query()["password"]
|
||||
if !ok || len(passwords[0]) < 1 {
|
||||
return "", false
|
||||
}
|
||||
|
||||
if passwords[0] != manager.password {
|
||||
manager.logger.Warn().Str("Password", passwords[0]).Msg("Wrong password: ")
|
||||
return "", false
|
||||
}
|
||||
|
||||
id, err := nanoid.NewIDSize(32)
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
return id, true
|
||||
}
|
||||
|
||||
func (manager *WebRTCManager) onConnected(session *session) error {
|
||||
if err := session.send(messageIdentityProvide{
|
||||
message: message{Event: "identity/provide"},
|
||||
ID: session.id,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (manager *WebRTCManager) onMessage(session *session, raw []byte) error {
|
||||
message := message{}
|
||||
if err := json.Unmarshal(raw, &message); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch message.Event {
|
||||
case "sdp/provide":
|
||||
return errors.Wrap(manager.createPeer(session, raw), "sdp/provide failed")
|
||||
case "control/release":
|
||||
return errors.Wrap(manager.controlRelease(session), "control/release failed")
|
||||
case "control/request":
|
||||
return errors.Wrap(manager.controlRequest(session), "control/request failed")
|
||||
default:
|
||||
manager.logger.Warn().Msgf("Unknown client method %s", message.Event)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (manager *WebRTCManager) handleWS(session *session) {
|
||||
bytes := make(chan []byte)
|
||||
cancel := make(chan struct{})
|
||||
ticker := time.NewTicker(pingPeriod)
|
||||
|
||||
go func() {
|
||||
defer func() {
|
||||
ticker.Stop()
|
||||
manager.logger.Info().Str("RemoteAddr", session.socket.RemoteAddr().String()).Msg("Handle WS ending")
|
||||
manager.destroy(session)
|
||||
}()
|
||||
|
||||
for {
|
||||
_, raw, err := session.socket.ReadMessage()
|
||||
if err != nil {
|
||||
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
|
||||
manager.logger.Warn().Err(err).Msg("ReadMessage error")
|
||||
}
|
||||
break
|
||||
}
|
||||
bytes <- raw
|
||||
}
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
case raw := <-bytes:
|
||||
manager.logger.Info().
|
||||
Str("ID", session.id).
|
||||
Str("Message", string(raw)).
|
||||
Msg("Reading from Websocket")
|
||||
if err := manager.onMessage(session, raw); err != nil {
|
||||
manager.logger.Error().Err(err).Msg("onClientMessage has failed")
|
||||
return
|
||||
}
|
||||
case <-cancel:
|
||||
return
|
||||
case _ = <-ticker.C:
|
||||
if err := session.socket.WriteMessage(websocket.PingMessage, nil); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (manager *WebRTCManager) destroy(session *session) {
|
||||
if manager.controller == session.id {
|
||||
manager.controller = ""
|
||||
for id, sess := range manager.sessions {
|
||||
if id != session.id {
|
||||
if err := sess.send(message{Event: "control/released"}); err != nil {
|
||||
manager.logger.Error().Err(err).Msg("session.send has failed")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := session.destroy(); err != nil {
|
||||
manager.logger.Error().Err(err).Msg("session.destroy has failed")
|
||||
}
|
||||
|
||||
delete(manager.sessions, session.id)
|
||||
}
|
||||
|
||||
func (manager *WebRTCManager) controlRelease(session *session) error {
|
||||
if manager.controller == session.id {
|
||||
manager.controller = ""
|
||||
|
||||
if err := session.send(message{Event: "control/release"}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for id, sess := range manager.sessions {
|
||||
if id != session.id {
|
||||
if err := sess.send(message{Event: "control/released"}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (manager *WebRTCManager) controlRequest(session *session) error {
|
||||
if manager.controller == "" {
|
||||
manager.controller = session.id
|
||||
|
||||
if err := session.send(message{Event: "control/give"}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for id, sess := range manager.sessions {
|
||||
if id != session.id {
|
||||
if err := sess.send(message{Event: "control/given"}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if err := session.send(message{Event: "control/locked"}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
controller, ok := manager.sessions[manager.controller]
|
||||
if ok {
|
||||
controller.send(message{Event: "control/requesting"})
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
Reference in New Issue
Block a user