diff --git a/cmd/ck-client/admin.go b/cmd/ck-client/admin.go index da1acf0..8d7ded3 100644 --- a/cmd/ck-client/admin.go +++ b/cmd/ck-client/admin.go @@ -4,25 +4,7 @@ package main // TODO: rewrite this. Think of another way of admin control -import ( - "crypto/aes" - "crypto/cipher" - "crypto/hmac" - "crypto/rand" - "crypto/sha256" - "encoding/base64" - "encoding/binary" - "encoding/json" - "errors" - "fmt" - "log" - "net" - - "github.com/cbeuw/Cloak/internal/client" - "github.com/cbeuw/Cloak/internal/client/TLS" - "github.com/cbeuw/Cloak/internal/util" -) - +/* type UserInfo struct { UID []byte // ALL of the following fields have to be accessed atomically @@ -53,7 +35,7 @@ func adminPrompt(sta *client.State) error { 4 addNewUser userinfo ok 5 delUser uid ok 6 syncMemFromDB uid ok - + 7 setSessionsCap uid cap ok 8 setUpRate uid rate ok 9 setDownRate uid rate ok @@ -276,3 +258,4 @@ func (a *administrator) checkAndDecrypt(data []byte) ([]byte, error) { stream.XORKeyStream(ret, ret) return ret, nil } +*/ diff --git a/cmd/ck-client/ck-client.go b/cmd/ck-client/ck-client.go index cea2be6..c484e7e 100644 --- a/cmd/ck-client/ck-client.go +++ b/cmd/ck-client/ck-client.go @@ -3,6 +3,7 @@ package main import ( + "encoding/base64" "flag" "fmt" "io" @@ -96,7 +97,7 @@ func main() { // The proxy port,should be 443 var remotePort string var config string - isAdmin := new(bool) + var b64AdminUID string log.SetFlags(log.LstdFlags | log.Lshortfile) @@ -114,8 +115,8 @@ func main() { flag.StringVar(&remoteHost, "s", "", "remoteHost: IP of your proxy server") flag.StringVar(&remotePort, "p", "443", "remotePort: proxy port, should be 443") flag.StringVar(&config, "c", "ckclient.json", "config: path to the configuration file or options seperated with semicolons") + flag.StringVar(&b64AdminUID, "a", "", "adminUID: enter the adminUID to serve the admin api") askVersion := flag.Bool("v", false, "Print the version number") - isAdmin = flag.Bool("a", false, "Admin mode") printUsage := flag.Bool("h", false, "Print this message") flag.Parse() @@ -132,19 +133,6 @@ func main() { log.Println("Starting standalone mode") } - if *isAdmin { - sta := client.InitState("", "", "", "", time.Now) - err := sta.ParseConfig(config) - if err != nil { - log.Fatal(err) - } - err = adminPrompt(sta) - if err != nil { - log.Println(err) - } - return - } - sta := client.InitState(localHost, localPort, remoteHost, remotePort, time.Now) err := sta.ParseConfig(config) if err != nil { @@ -164,28 +152,40 @@ func main() { if sta.TicketTimeHint == 0 { log.Fatal("TicketTimeHint cannot be empty or 0") } + listeningIP := sta.LocalHost if net.ParseIP(listeningIP).To4() == nil { // IPv6 needs square brackets listeningIP = "[" + listeningIP + "]" } listener, err := net.Listen("tcp", listeningIP+":"+sta.LocalPort) - log.Println("Listening for proxy clients on " + listeningIP + ":" + sta.LocalPort) + log.Println("Listening on " + listeningIP + ":" + sta.LocalPort) if err != nil { log.Fatal(err) } + var adminUID []byte + if b64AdminUID != "" { + adminUID, err = base64.StdEncoding.DecodeString(b64AdminUID) + if err != nil { + log.Fatal(err) + } + } + start: log.Println("Attemtping to start a new session") // sessionID is usergenerated. There shouldn't be a security concern because the scope of // sessionID is limited to its UID. rand.Seed(time.Now().UnixNano()) sessionID := rand.Uint32() - sta.SetSessionID(sessionID) - var UNLIMITED_DOWN int64 = 1e15 - var UNLIMITED_UP int64 = 1e15 - valve := mux.MakeValve(1e12, 1e12, &UNLIMITED_DOWN, &UNLIMITED_UP) + if adminUID != nil { + sessionID = 0 + sta.UID = adminUID + sta.NumConn = 1 + } + + sta.SetSessionID(sessionID) var crypto mux.Crypto switch sta.EncryptionMethod { case 0x00: @@ -207,7 +207,8 @@ start: sessionKey := make([]byte, 32) rand.Read(sessionKey) sta.SessionKey = sessionKey - sesh := mux.MakeSession(sessionID, valve, mux.MakeObfs(sta.SessionKey, crypto), mux.MakeDeobfs(sta.SessionKey, crypto), util.ReadTLS) + + sesh := mux.MakeSession(sessionID, mux.UNLIMITED_VALVE, mux.MakeObfs(sta.SessionKey, crypto), mux.MakeDeobfs(sta.SessionKey, crypto), util.ReadTLS) var wg sync.WaitGroup for i := 0; i < sta.NumConn; i++ { diff --git a/cmd/ck-server/ck-server.go b/cmd/ck-server/ck-server.go index d97deef..6aab9e9 100644 --- a/cmd/ck-server/ck-server.go +++ b/cmd/ck-server/ck-server.go @@ -1,12 +1,14 @@ package main import ( + "bytes" "encoding/base64" "flag" "fmt" "io" "log" "net" + "net/http" "os" "strings" "time" @@ -81,70 +83,6 @@ func dispatchConnection(conn net.Conn, sta *server.State) { return } - finishHandshake := func() error { - reply := server.ComposeReply(ch) - _, err = conn.Write(reply) - if err != nil { - go conn.Close() - return err - } - - // Two discarded messages: ChangeCipherSpec and Finished - discardBuf := make([]byte, 1024) - for c := 0; c < 2; c++ { - _, err = util.ReadTLS(conn, discardBuf) - if err != nil { - go conn.Close() - return err - } - } - return nil - } - - /* - // adminUID can use the server as normal with unlimited QoS credits. The adminUID is not - // added to the userinfo database. The distinction between going into the admin mode - // and normal proxy mode is that sessionID needs == 0 for admin mode - if bytes.Equal(UID, sta.AdminUID) && sessionID == 0 { - err = finishHandshake() - if err != nil { - log.Println(err) - return - } - c := sta.Userpanel.MakeController(sta.AdminUID) - for { - n, err := conn.Read(data) - if err != nil { - log.Println(err) - return - } - resp, err := c.HandleRequest(data[:n]) - if err != nil { - log.Println(err) - } - _, err = conn.Write(resp) - if err != nil { - log.Println(err) - return - } - } - - } - */ - - user, err := sta.Panel.GetUser(UID) - if err != nil { - log.Printf("+1 unauthorised user from %v, uid: %x\n", conn.RemoteAddr(), UID) - goWeb(data) - return - } - - err = finishHandshake() - if err != nil { - log.Println(err) - return - } - var crypto mux.Crypto switch encryptionMethod { case 0x00: @@ -169,7 +107,62 @@ func dispatchConnection(conn net.Conn, sta *server.State) { return } - sesh, existing, err := user.GetSession(sessionID, mux.MakeObfs(sessionKey, crypto), mux.MakeDeobfs(sessionKey, crypto), util.ReadTLS) + obfs := mux.MakeObfs(sessionKey, crypto) + deobfs := mux.MakeDeobfs(sessionKey, crypto) + + finishHandshake := func() error { + reply := server.ComposeReply(ch) + _, err = conn.Write(reply) + if err != nil { + go conn.Close() + return err + } + + // Two discarded messages: ChangeCipherSpec and Finished + discardBuf := make([]byte, 1024) + for c := 0; c < 2; c++ { + _, err = util.ReadTLS(conn, discardBuf) + if err != nil { + go conn.Close() + return err + } + } + return nil + } + + // adminUID can use the server as normal with unlimited QoS credits. The adminUID is not + // added to the userinfo database. The distinction between going into the admin mode + // and normal proxy mode is that sessionID needs == 0 for admin mode + if bytes.Equal(UID, sta.AdminUID) && sessionID == 0 { + err = finishHandshake() + if err != nil { + log.Println(err) + return + } + sesh := mux.MakeSession(0, mux.UNLIMITED_VALVE, obfs, deobfs, util.ReadTLS) + sesh.AddConnection(conn) + //TODO: Router could be nil in cnc mode + err = http.Serve(sesh, sta.LocalAPIRouter) + if err != nil { + log.Println(err) + return + } + } + + user, err := sta.Panel.GetUser(UID) + if err != nil { + log.Printf("+1 unauthorised user from %v, uid: %v\n", conn.RemoteAddr(), base64.StdEncoding.EncodeToString(UID)) + goWeb(data) + return + } + + err = finishHandshake() + if err != nil { + log.Println(err) + return + } + + sesh, existing, err := user.GetSession(sessionID, obfs, deobfs, util.ReadTLS) if err != nil { user.DelSession(sessionID) log.Println(err) diff --git a/go.mod b/go.mod index ebe340f..204af24 100644 --- a/go.mod +++ b/go.mod @@ -2,6 +2,7 @@ module github.com/cbeuw/Cloak require ( github.com/boltdb/bolt v1.3.1 + github.com/gorilla/mux v1.7.3 github.com/juju/ratelimit v1.0.1 github.com/kr/pretty v0.1.0 // indirect golang.org/x/crypto v0.0.0-20190123085648-057139ce5d2b diff --git a/go.sum b/go.sum index e3bba69..ae0515a 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4= github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps= +github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw= +github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/juju/ratelimit v1.0.1 h1:+7AIFJVQ0EQgq/K9+0Krm7m530Du7tIz0METWzN0RgY= github.com/juju/ratelimit v1.0.1/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= diff --git a/internal/multiplex/qos.go b/internal/multiplex/qos.go index a22d584..f01dd39 100644 --- a/internal/multiplex/qos.go +++ b/internal/multiplex/qos.go @@ -21,16 +21,19 @@ type Valve struct { tx *int64 } -func MakeValve(rxRate, txRate int64, rx, tx *int64) *Valve { +func MakeValve(rxRate, txRate int64) *Valve { + var rx, tx int64 v := &Valve{ - rx: rx, - tx: tx, + rx: &rx, + tx: &tx, } v.SetRxRate(rxRate) v.SetTxRate(txRate) return v } +var UNLIMITED_VALVE = MakeValve(1<<63-1, 1<<63-1) + func (v *Valve) SetRxRate(rate int64) { v.rxtb.Store(ratelimit.NewBucketWithRate(float64(rate), rate)) } func (v *Valve) SetTxRate(rate int64) { v.txtb.Store(ratelimit.NewBucketWithRate(float64(rate), rate)) } func (v *Valve) rxWait(n int) { v.rxtb.Load().(*ratelimit.Bucket).Wait(int64(n)) } diff --git a/internal/multiplex/session.go b/internal/multiplex/session.go index d8bc76c..c4a590f 100644 --- a/internal/multiplex/session.go +++ b/internal/multiplex/session.go @@ -35,6 +35,8 @@ type Session struct { // Switchboard manages all connections to remote sb *switchboard + addrs atomic.Value + // For accepting new streams acceptCh chan *Stream @@ -56,6 +58,7 @@ func MakeSession(id uint32, valve *Valve, obfs Obfser, deobfs Deobfser, obfsedRe acceptCh: make(chan *Stream, acceptBacklog), die: make(chan struct{}), } + sesh.addrs.Store([]net.Addr{nil, nil}) sesh.sb = makeSwitchboard(sesh, valve) go sesh.timeoutAfter(30 * time.Second) return sesh @@ -63,6 +66,8 @@ func MakeSession(id uint32, valve *Valve, obfs Obfser, deobfs Deobfser, obfsedRe func (sesh *Session) AddConnection(conn net.Conn) { sesh.sb.addConn(conn) + addrs := []net.Addr{conn.LocalAddr(), conn.RemoteAddr()} + sesh.addrs.Store(addrs) } func (sesh *Session) OpenStream() (*Stream, error) { @@ -174,5 +179,4 @@ func (sesh *Session) timeoutAfter(to time.Duration) { } } -// Addr is only for implementing net.Listener -func (sesh *Session) Addr() net.Addr { return nil } +func (sesh *Session) Addr() net.Addr { return sesh.addrs.Load().([]net.Addr)[0] } diff --git a/internal/multiplex/stream.go b/internal/multiplex/stream.go index bb7481a..ff7f76a 100644 --- a/internal/multiplex/stream.go +++ b/internal/multiplex/stream.go @@ -2,6 +2,8 @@ package multiplex import ( "errors" + "io" + "log" "net" "time" @@ -29,6 +31,8 @@ type Stream struct { newFrameCh chan *Frame // sortedBufCh are order-sorted data ready to be read raw sortedBufCh chan []byte + feederR *io.PipeReader + feederW *io.PipeWriter // atomic nextSendSeq uint32 @@ -41,6 +45,7 @@ type Stream struct { } func makeStream(id uint32, sesh *Session) *Stream { + r, w := io.Pipe() stream := &Stream{ id: id, session: sesh, @@ -48,11 +53,32 @@ func makeStream(id uint32, sesh *Session) *Stream { sh: []*frameNode{}, newFrameCh: make(chan *Frame, 1024), sortedBufCh: make(chan []byte, 1024), + feederR: r, + feederW: w, } go stream.recvNewFrame() + go stream.feed() return stream } +func (stream *Stream) feed() { + for { + select { + case <-stream.die: + return + case data := <-stream.sortedBufCh: + if len(data) == 0 { + stream.passiveClose() + return + } + _, err := stream.feederW.Write(data) + if err != nil { + log.Println(err) + } + } + } +} + func (stream *Stream) Read(buf []byte) (n int, err error) { if len(buf) == 0 { select { @@ -65,16 +91,8 @@ func (stream *Stream) Read(buf []byte) (n int, err error) { select { case <-stream.die: return 0, ErrBrokenStream - case data := <-stream.sortedBufCh: - if len(data) == 0 { - stream.passiveClose() - return 0, ErrBrokenStream - } - if len(buf) < len(data) { - return 0, errors.New("buf too small") - } - copy(buf, data) - return len(data), nil + default: + return stream.feederR.Read(buf) } } @@ -163,8 +181,8 @@ func (stream *Stream) closeNoDelMap() { // they are not used var errNotImplemented = errors.New("Not implemented") -func (stream *Stream) LocalAddr() net.Addr { return nil } -func (stream *Stream) RemoteAddr() net.Addr { return nil } +func (stream *Stream) LocalAddr() net.Addr { return stream.session.addrs.Load().([]net.Addr)[0] } +func (stream *Stream) RemoteAddr() net.Addr { return stream.session.addrs.Load().([]net.Addr)[1] } // TODO: implement the following func (stream *Stream) SetDeadline(t time.Time) error { return errNotImplemented } diff --git a/internal/server/activeuser.go b/internal/server/activeuser.go index f702fa4..24b0b64 100644 --- a/internal/server/activeuser.go +++ b/internal/server/activeuser.go @@ -36,7 +36,7 @@ func (u *ActiveUser) GetSession(sessionID uint32, obfs mux.Obfser, deobfs mux.De u.sessionsM.Unlock() return sesh, true, nil } else { - err := u.panel.manager.authoriseNewSession(u) + err := u.panel.Manager.authoriseNewSession(u) if err != nil { u.sessionsM.Unlock() return nil, false, err diff --git a/internal/server/state.go b/internal/server/state.go index 5ca0920..98f693b 100644 --- a/internal/server/state.go +++ b/internal/server/state.go @@ -8,6 +8,8 @@ import ( "io/ioutil" "sync" "time" + + gmux "github.com/gorilla/mux" ) type rawConfig struct { @@ -36,7 +38,8 @@ type State struct { usedRandomM sync.RWMutex usedRandom map[[32]byte]int - Panel *userPanel + Panel *userPanel + LocalAPIRouter *gmux.Router } func InitState(bindHost, bindPort string, nowFunc func() time.Time) (*State, error) { @@ -76,6 +79,7 @@ func (sta *State) ParseConfig(conf string) (err error) { return err } sta.Panel = MakeUserPanel(manager) + sta.LocalAPIRouter = manager.Router } sta.RedirAddr = preParse.RedirAddr diff --git a/internal/server/swagger.yaml b/internal/server/swagger.yaml new file mode 100644 index 0000000..8de28b9 --- /dev/null +++ b/internal/server/swagger.yaml @@ -0,0 +1,159 @@ +swagger: '2.0' +info: + description: | + This is the API of Cloak server + version: 1.0.0 + title: Cloak Server + contact: + email: cbeuw.andy@gmail.com + license: + name: GPLv3 + url: https://www.gnu.org/licenses/gpl-3.0.en.html +# host: petstore.swagger.io +# basePath: /v2 +tags: + - name: admin + description: Endpoints used by the host administrators + - name: users + description: Operations related to user controls by admin +# schemes: +# - http +paths: + /admin/users: + get: + tags: + - admin + - users + summary: Show all users + description: Returns an array of all UserInfo + operationId: listAllUsers + produces: + - application/json + responses: + 200: + description: successful operation + schema: + type: array + items: + $ref: '#/definitions/UserInfo' + 500: + description: internal error + /admin/users/{UID}: + get: + tags: + - admin + - users + summary: Show userinfo by UID + description: Returns a UserInfo object + operationId: getUserInfo + produces: + - application/json + parameters: + - name: UID + in: path + description: UID of the user + required: true + type: string + format: byte + responses: + 200: + description: successful operation + schema: + $ref: '#/definitions/UserInfo' + 400: + description: bad request + 404: + description: User not found + 500: + description: internal error + post: + tags: + - admin + - users + summary: Updates the userinfo of the specified user, if the user does not exist, then a new user is created + operationId: writeUserInfo + consumes: + - application/json + produces: + - application/json + parameters: + - name: UID + in: path + description: UID of the user + required: true + type: string + format: byte + - name: UserInfo + in: body + description: New userinfo + required: true + schema: + type: array + items: + $ref: '#/definitions/UserInfo' + responses: + 201: + description: successful operation + 400: + description: bad request + 500: + description: internal error + delete: + tags: + - admin + - users + summary: Deletes a user + operationId: deleteUser + produces: + - application/json + parameters: + - name: UID + in: path + description: UID of the user to be deleted + required: true + type: string + format: byte + responses: + 200: + description: successful operation + 400: + description: bad request + 404: + description: User not found + 500: + description: internal error + +definitions: + UserInfo: + type: object + properties: + UID: + type: string + format: byte + SessionsCap: + type: integer + format: int32 + UpRate: + type: integer + format: int64 + DownRate: + type: integer + format: int64 + UpCredit: + type: integer + format: int64 + DownCredit: + type: integer + format: int64 + ExpiryTime: + type: integer + format: int64 +externalDocs: + description: Find out more about Swagger + url: http://swagger.io +# Added by API Auto Mocking Plugin +host: virtserver.swaggerhub.com +basePath: /cbeuw/ck-server/1.0.0 +schemes: + - https + - http \ No newline at end of file diff --git a/internal/server/um_local.go b/internal/server/um_local.go index 64342f4..95156ee 100644 --- a/internal/server/um_local.go +++ b/internal/server/um_local.go @@ -6,6 +6,7 @@ import ( "time" "github.com/boltdb/bolt" + gmux "github.com/gorilla/mux" ) var Uint32 = binary.BigEndian.Uint32 @@ -13,8 +14,20 @@ var Uint64 = binary.BigEndian.Uint64 var PutUint32 = binary.BigEndian.PutUint32 var PutUint64 = binary.BigEndian.PutUint64 +func i64ToB(value int64) []byte { + oct := make([]byte, 8) + PutUint64(oct, uint64(value)) + return oct +} +func i32ToB(value int32) []byte { + nib := make([]byte, 4) + PutUint32(nib, uint32(value)) + return nib +} + type localManager struct { - db *bolt.DB + db *bolt.DB + Router *gmux.Router } func MakeLocalManager(dbPath string) (*localManager, error) { @@ -22,7 +35,20 @@ func MakeLocalManager(dbPath string) (*localManager, error) { if err != nil { return nil, err } - return &localManager{db}, nil + ret := &localManager{ + db: db, + } + ret.Router = ret.registerMux() + return ret, nil +} + +func (manager *localManager) registerMux() *gmux.Router { + r := gmux.NewRouter() + r.HandleFunc("/admin/users", manager.listAllUsersHlr).Methods("GET") + r.HandleFunc("/admin/users/{UID}", manager.getUserInfo).Methods("GET") + r.HandleFunc("/admin/users/{UID}", manager.writeUserInfo).Methods("POST") + r.HandleFunc("/admin/users/{UID}", manager.deleteUser).Methods("DELETE") + return r } func (manager *localManager) authenticateUser(UID []byte) (int64, int64, error) { @@ -90,12 +116,6 @@ func (manager *localManager) authoriseNewSession(user *ActiveUser) error { return nil } -func i64ToB(value int64) []byte { - oct := make([]byte, 8) - PutUint64(oct, uint64(value)) - return oct -} - func (manager *localManager) uploadStatus(uploads []statusUpdate) ([]statusResponse, error) { var responses []statusResponse err := manager.db.Update(func(tx *bolt.Tx) error { @@ -103,7 +123,12 @@ func (manager *localManager) uploadStatus(uploads []statusUpdate) ([]statusRespo var resp statusResponse bucket := tx.Bucket(status.UID) if bucket == nil { - log.Printf("%x doesn't exist\n", status.UID) + resp = statusResponse{ + status.UID, + TERMINATE, + "User no longer exists", + } + responses = append(responses, resp) continue } @@ -115,8 +140,14 @@ func (manager *localManager) uploadStatus(uploads []statusUpdate) ([]statusRespo TERMINATE, "No upload credit left", } + responses = append(responses, resp) + continue + } + err := bucket.Put([]byte("UpCredit"), i64ToB(newUp)) + if err != nil { + log.Println(err) + continue } - bucket.Put([]byte("UpCredit"), i64ToB(newUp)) oldDown := int64(Uint64(bucket.Get([]byte("DownCredit")))) newDown := oldDown - status.downUsage @@ -126,20 +157,24 @@ func (manager *localManager) uploadStatus(uploads []statusUpdate) ([]statusRespo TERMINATE, "No download credit left", } + responses = append(responses, resp) + continue + } + err = bucket.Put([]byte("DownCredit"), i64ToB(newDown)) + if err != nil { + log.Println(err) + continue } - bucket.Put([]byte("DownCredit"), i64ToB(newDown)) expiry := int64(Uint64(bucket.Get([]byte("ExpiryTime")))) - if time.Now().Unix()>expiry{ + if time.Now().Unix() > expiry { resp = statusResponse{ status.UID, TERMINATE, "User has expired", } - } - - if resp.UID != nil { responses = append(responses, resp) + continue } } return nil diff --git a/internal/server/um_local_api.go b/internal/server/um_local_api.go new file mode 100644 index 0000000..89a3ce0 --- /dev/null +++ b/internal/server/um_local_api.go @@ -0,0 +1,165 @@ +package server + +import ( + "bytes" + "encoding/base64" + "encoding/json" + "github.com/boltdb/bolt" + "net/http" + + gmux "github.com/gorilla/mux" +) + +type UserInfo struct { + UID []byte + SessionsCap int + UpRate int64 + DownRate int64 + UpCredit int64 + DownCredit int64 + ExpiryTime int64 +} + +func (manager *localManager) listAllUsersHlr(w http.ResponseWriter, r *http.Request) { + var infos []UserInfo + _ = manager.db.View(func(tx *bolt.Tx) error { + err := tx.ForEach(func(UID []byte, bucket *bolt.Bucket) error { + var uinfo UserInfo + uinfo.UID = UID + uinfo.SessionsCap = int(Uint32(bucket.Get([]byte("SessionsCap")))) + uinfo.UpRate = int64(Uint64(bucket.Get([]byte("UpRate")))) + uinfo.DownRate = int64(Uint64(bucket.Get([]byte("DownRate")))) + uinfo.UpCredit = int64(Uint64(bucket.Get([]byte("UpCredit")))) + uinfo.DownCredit = int64(Uint64(bucket.Get([]byte("DownCredit")))) + uinfo.ExpiryTime = int64(Uint64(bucket.Get([]byte("ExpiryTime")))) + infos = append(infos, uinfo) + return nil + }) + return err + }) + resp, err := json.Marshal(infos) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + _, _ = w.Write(resp) +} + +func (manager *localManager) getUserInfo(w http.ResponseWriter, r *http.Request) { + b64UID := gmux.Vars(r)["UID"] + if b64UID == "" { + http.Error(w, "UID cannot be empty", http.StatusBadRequest) + } + + UID, err := base64.URLEncoding.DecodeString(b64UID) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + var uinfo UserInfo + err = manager.db.View(func(tx *bolt.Tx) error { + bucket := tx.Bucket([]byte(UID)) + if bucket == nil { + return ErrUserNotFound + } + uinfo.UID = UID + uinfo.SessionsCap = int(Uint32(bucket.Get([]byte("SessionsCap")))) + uinfo.UpRate = int64(Uint64(bucket.Get([]byte("UpRate")))) + uinfo.DownRate = int64(Uint64(bucket.Get([]byte("DownRate")))) + uinfo.UpCredit = int64(Uint64(bucket.Get([]byte("UpCredit")))) + uinfo.DownCredit = int64(Uint64(bucket.Get([]byte("DownCredit")))) + uinfo.ExpiryTime = int64(Uint64(bucket.Get([]byte("ExpiryTime")))) + return nil + }) + if err == ErrUserNotFound { + http.Error(w, ErrUserNotFound.Error(), http.StatusNotFound) + return + } + resp, err := json.Marshal(uinfo) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + _, _ = w.Write(resp) +} + +func (manager *localManager) writeUserInfo(w http.ResponseWriter, r *http.Request) { + b64UID := gmux.Vars(r)["UID"] + if b64UID == "" { + http.Error(w, "UID cannot be empty", http.StatusBadRequest) + return + } + UID, err := base64.URLEncoding.DecodeString(b64UID) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + jsonUinfo := gmux.Vars(r)["UserInfo"] + if jsonUinfo == "" { + http.Error(w, "UserInfo cannot be empty", http.StatusBadRequest) + return + } + var uinfo UserInfo + err = json.Unmarshal([]byte(jsonUinfo), &uinfo) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + if !bytes.Equal(UID, uinfo.UID) { + http.Error(w, "UID mismatch", http.StatusBadRequest) + } + + err = manager.db.Update(func(tx *bolt.Tx) error { + bucket, err := tx.CreateBucket(uinfo.UID) + if err != nil { + return err + } + if err = bucket.Put([]byte("SessionsCap"), i32ToB(int32(uinfo.SessionsCap))); err != nil { + return err + } + if err = bucket.Put([]byte("UpRate"), i64ToB(uinfo.UpRate)); err != nil { + return err + } + if err = bucket.Put([]byte("DownRate"), i64ToB(uinfo.DownRate)); err != nil { + return err + } + if err = bucket.Put([]byte("UpCredit"), i64ToB(uinfo.UpCredit)); err != nil { + return err + } + if err = bucket.Put([]byte("DownCredit"), i64ToB(uinfo.DownCredit)); err != nil { + return err + } + if err = bucket.Put([]byte("ExpiryTime"), i64ToB(uinfo.ExpiryTime)); err != nil { + return err + } + return nil + }) + + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + w.WriteHeader(http.StatusCreated) +} + +func (manager *localManager) deleteUser(w http.ResponseWriter, r *http.Request) { + b64UID := gmux.Vars(r)["UID"] + if b64UID == "" { + http.Error(w, "UID cannot be empty", http.StatusBadRequest) + return + } + UID, err := base64.URLEncoding.DecodeString(b64UID) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + err = manager.db.Update(func(tx *bolt.Tx) error { + return tx.DeleteBucket(UID) + }) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + w.WriteHeader(http.StatusOK) +} diff --git a/internal/server/userpanel.go b/internal/server/userpanel.go index ee33a48..9efade4 100644 --- a/internal/server/userpanel.go +++ b/internal/server/userpanel.go @@ -10,7 +10,7 @@ import ( ) type userPanel struct { - manager UserManager + Manager UserManager activeUsersM sync.RWMutex activeUsers map[[16]byte]*ActiveUser @@ -20,7 +20,7 @@ type userPanel struct { func MakeUserPanel(manager UserManager) *userPanel { ret := &userPanel{ - manager: manager, + Manager: manager, activeUsers: make(map[[16]byte]*ActiveUser), usageUpdateQueue: make(map[[16]byte]*usagePair), } @@ -37,13 +37,12 @@ func (panel *userPanel) GetUser(UID []byte) (*ActiveUser, error) { return user, nil } - upRate, downRate, err := panel.manager.authenticateUser(UID) + upRate, downRate, err := panel.Manager.authenticateUser(UID) if err != nil { panel.activeUsersM.Unlock() return nil, err } - var upUsage, downUsage int64 - valve := mux.MakeValve(upRate, downRate, &upUsage, &downUsage) + valve := mux.MakeValve(upRate, downRate) user := &ActiveUser{ panel: panel, valve: valve, @@ -124,7 +123,7 @@ func (panel *userPanel) commitUpdate() { statuses = append(statuses, status) } - responses, err := panel.manager.uploadStatus(statuses) + responses, err := panel.Manager.uploadStatus(statuses) if err != nil { log.Println(err) }