<commit_before>// Copyright (c) 2014 Dataence, LLC. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package service

import (
	"encoding/binary"
	"fmt"
	"io"
	"net"
	"time"

	"github.com/dataence/glog"
	"github.com/gorilla/websocket"
	"github.com/surge/surgemq/message"
)

// receiver() reads data from the network, and writes the data into the incoming buffer
func (this *service) receiver() {
	defer func() {
		// Let's recover from panic
		if r := recover(); r != nil {
			glog.Errorf("(%s) Recovering from panic: %v", this.cid(), r)
		}

		this.wgStopped.Done()

		glog.Debugf("(%s) Stopping receiver", this.cid())
	}()

	glog.Debugf("(%s) Starting receiver", this.cid())

	this.wgStarted.Done()

	switch conn := this.conn.(type) {
	case net.Conn:
		//glog.Debugf("server/handleConnection: Setting read deadline to %d", time.Second*time.Duration(this.keepAlive))
		conn.SetReadDeadline(time.Now().Add(time.Second * time.Duration(this.keepAlive)))

		for {
			_, err := this.in.ReadFrom(conn)

			if err != nil {
				if err != io.EOF {
					glog.Errorf("(%s) error reading from connection: %v", this.cid(), err)
				}
				return
			}
		}

	case *websocket.Conn:
		glog.Errorf("(%s) Websocket: %v", this.cid(), ErrInvalidConnectionType)

	default:
		glog.Errorf("(%s) %v", this.cid(), ErrInvalidConnectionType)
	}
}

// sender() writes data from the outgoing buffer to the network
func (this *service) sender() {
	defer func() {
		// Let's recover from panic
		if r := recover(); r != nil {
			glog.Errorf("(%s) Recovering from panic: %v", this.cid(), r)
		}

		this.wgStopped.Done()

		glog.Debugf("(%s) Stopping sender", this.cid())
	}()

	glog.Debugf("(%s) Starting sender", this.cid())

	this.wgStarted.Done()

	switch conn := this.conn.(type) {
	case net.Conn:
		for {
			_, err := this.out.WriteTo(conn)

			if err != nil {
				if err != io.EOF {
					glog.Errorf("(%s) error writing data: %v", this.cid(), err)
				}
				return
			}
		}

	case *websocket.Conn:
		glog.Errorf("(%s) Websocket not supported", this.cid())

	default:
		glog.Errorf("(%s) Invalid connection type", this.cid())
	}
}

// peekMessageSize() reads, but not commits, enough bytes to determine the size of
// the next message and returns the type and size.
func (this *service) peekMessageSize() (message.MessageType, int, error) {
	var (
		b   []byte
		err error
		cnt int = 2
	)

	if this.in == nil {
		err = ErrBufferNotReady
		return 0, 0, err
	}

	// Let's read enough bytes to get the message header (msg type, remaining length)
	for {
		// If we have read 5 bytes and still not done, then there's a problem.
		if cnt > 5 {
			return 0, 0, fmt.Errorf("sendrecv/peekMessageSize: 4th byte of remaining length has continuation bit set")
		}

		// Peek cnt bytes from the input buffer.
		b, err = this.in.ReadWait(cnt)
		if err != nil {
			return 0, 0, err
		}

		// If not enough bytes are returned, then continue until there's enough.
		if len(b) < cnt {
			continue
		}

		// If we got enough bytes, then check the last byte to see if the continuation
		// bit is set. If so, increment cnt and continue peeking
		if b[cnt-1] >= 0x80 {
			cnt++
		} else {
			break
		}
	}

	// Get the remaining length of the message
	remlen, m := binary.Uvarint(b[1:])

	// Total message length is remlen + 1 (msg type) + m (remlen bytes)
	total := int(remlen) + 1 + m

	mtype := message.MessageType(b[0] >> 4)

	return mtype, total, err
}

// peekMessage() reads a message from the buffer, but the bytes are NOT committed.
// This means the buffer still thinks the bytes are not read yet.
func (this *service) peekMessage(mtype message.MessageType, total int) (message.Message, int, error) {
	var (
		b    []byte
		err  error
		i, n int
		msg  message.Message
	)

	if this.in == nil {
		return nil, 0, ErrBufferNotReady
	}

	// Peek until we get total bytes
	for i = 0; ; i++ {
		// Peek remlen bytes from the input buffer.
		b, err = this.in.ReadWait(total)
		if err != nil && err != ErrBufferInsufficientData {
			return nil, 0, err
		}

		// If not enough bytes are returned, then continue until there's enough.
		if len(b) >= total {
			break
		}
	}

	msg, err = mtype.New()
	if err != nil {
		return nil, 0, err
	}

	n, err = msg.Decode(b)
	return msg, n, err
}

// readMessage() reads and copies a message from the buffer. The buffer bytes are
// committed as a result of the read.
func (this *service) readMessage(mtype message.MessageType, total int) (message.Message, int, error) {
	var (
		b   []byte
		err error
		n   int
		msg message.Message
	)

	if this.in == nil {
		err = ErrBufferNotReady
		return nil, 0, err
	}

	if len(this.intmp) < total {
		this.intmp = make([]byte, total)
	}

	// Read until we get total bytes
	l := 0
	for l < total {
		n, err = this.in.Read(this.intmp[l:])
		l += n
		glog.Debugf("read %d bytes, total %d", n, l)
		if err != nil {
			return nil, 0, err
		}
	}

	b = this.intmp[:total]

	msg, err = mtype.New()
	if err != nil {
		return msg, 0, err
	}

	n, err = msg.Decode(b)
	return msg, n, err
}

// writeMessage() writes a message to the outgoing buffer
func (this *service) writeMessage(msg message.Message) (int, error) {
	var (
		l    int = msg.Len()
		m, n int
		err  error
		buf  []byte
		wrap bool
	)

	if this.out == nil {
		return 0, ErrBufferNotReady
	}

	// This is to serialize writes to the underlying buffer. Multiple goroutines could
	// potentially get here because of calling Publish() or Subscribe() or other
	// functions that will send messages. For example, if a message is received in
	// another connetion, and the message needs to be published to this client, then
	// the Publish() function is called, and at the same time, another client could
	// do exactly the same thing.
	//
	// Not an ideal fix though. If possible we should remove mutex and be lockfree.
	// Mainly because when there's a large number of goroutines that want to publish
	// to this client, then they will all block. However, this will do for now.
	//
	// FIXME: Try to find a better way than a mutex...if possible.
	this.wmu.Lock()
	defer this.wmu.Unlock()

	buf, wrap, err = this.out.WriteWait(l)
	if err != nil {
		return 0, err
	}

	if wrap {
		if len(this.outtmp) < l {
			this.outtmp = make([]byte, l)
		}

		n, err = msg.Encode(this.outtmp[0:])
		if err != nil {
			return 0, err
		}

		m, err = this.out.Write(this.outtmp[0:n])
		if err != nil {
			return m, err
		}
	} else {
		n, err = msg.Encode(buf[0:])
		if err != nil {
			return 0, err
		}

		m, err = this.out.WriteCommit(n)
		if err != nil {
			return 0, err
		}
	}

	this.outStat.increment(int64(m))

	return m, nil
}
<commit_msg>fixed #5 removed websocket for now<commit_after>// Copyright (c) 2014 Dataence, LLC. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package service

import (
	"encoding/binary"
	"fmt"
	"io"
	"net"
	"time"

	"github.com/dataence/glog"
	"github.com/surge/surgemq/message"
)

// receiver() reads data from the network, and writes the data into the incoming buffer
func (this *service) receiver() {
	defer func() {
		// Let's recover from panic
		if r := recover(); r != nil {
			glog.Errorf("(%s) Recovering from panic: %v", this.cid(), r)
		}

		this.wgStopped.Done()

		glog.Debugf("(%s) Stopping receiver", this.cid())
	}()

	glog.Debugf("(%s) Starting receiver", this.cid())

	this.wgStarted.Done()

	switch conn := this.conn.(type) {
	case net.Conn:
		//glog.Debugf("server/handleConnection: Setting read deadline to %d", time.Second*time.Duration(this.keepAlive))
		conn.SetReadDeadline(time.Now().Add(time.Second * time.Duration(this.keepAlive)))

		for {
			_, err := this.in.ReadFrom(conn)

			if err != nil {
				if err != io.EOF {
					glog.Errorf("(%s) error reading from connection: %v", this.cid(), err)
				}
				return
			}
		}

	//case *websocket.Conn:
	//	glog.Errorf("(%s) Websocket: %v", this.cid(), ErrInvalidConnectionType)

	default:
		glog.Errorf("(%s) %v", this.cid(), ErrInvalidConnectionType)
	}
}

// sender() writes data from the outgoing buffer to the network
func (this *service) sender() {
	defer func() {
		// Let's recover from panic
		if r := recover(); r != nil {
			glog.Errorf("(%s) Recovering from panic: %v", this.cid(), r)
		}

		this.wgStopped.Done()

		glog.Debugf("(%s) Stopping sender", this.cid())
	}()

	glog.Debugf("(%s) Starting sender", this.cid())

	this.wgStarted.Done()

	switch conn := this.conn.(type) {
	case net.Conn:
		for {
			_, err := this.out.WriteTo(conn)

			if err != nil {
				if err != io.EOF {
					glog.Errorf("(%s) error writing data: %v", this.cid(), err)
				}
				return
			}
		}

	//case *websocket.Conn:
	//	glog.Errorf("(%s) Websocket not supported", this.cid())

	default:
		glog.Errorf("(%s) Invalid connection type", this.cid())
	}
}

// peekMessageSize() reads, but not commits, enough bytes to determine the size of
// the next message and returns the type and size.
func (this *service) peekMessageSize() (message.MessageType, int, error) {
	var (
		b   []byte
		err error
		cnt int = 2
	)

	if this.in == nil {
		err = ErrBufferNotReady
		return 0, 0, err
	}

	// Let's read enough bytes to get the message header (msg type, remaining length)
	for {
		// If we have read 5 bytes and still not done, then there's a problem.
		if cnt > 5 {
			return 0, 0, fmt.Errorf("sendrecv/peekMessageSize: 4th byte of remaining length has continuation bit set")
		}

		// Peek cnt bytes from the input buffer.
		b, err = this.in.ReadWait(cnt)
		if err != nil {
			return 0, 0, err
		}

		// If not enough bytes are returned, then continue until there's enough.
		if len(b) < cnt {
			continue
		}

		// If we got enough bytes, then check the last byte to see if the continuation
		// bit is set. If so, increment cnt and continue peeking
		if b[cnt-1] >= 0x80 {
			cnt++
		} else {
			break
		}
	}

	// Get the remaining length of the message
	remlen, m := binary.Uvarint(b[1:])

	// Total message length is remlen + 1 (msg type) + m (remlen bytes)
	total := int(remlen) + 1 + m

	mtype := message.MessageType(b[0] >> 4)

	return mtype, total, err
}

// peekMessage() reads a message from the buffer, but the bytes are NOT committed.
// This means the buffer still thinks the bytes are not read yet.
func (this *service) peekMessage(mtype message.MessageType, total int) (message.Message, int, error) {
	var (
		b    []byte
		err  error
		i, n int
		msg  message.Message
	)

	if this.in == nil {
		return nil, 0, ErrBufferNotReady
	}

	// Peek until we get total bytes
	for i = 0; ; i++ {
		// Peek remlen bytes from the input buffer.
		b, err = this.in.ReadWait(total)
		if err != nil && err != ErrBufferInsufficientData {
			return nil, 0, err
		}

		// If not enough bytes are returned, then continue until there's enough.
		if len(b) >= total {
			break
		}
	}

	msg, err = mtype.New()
	if err != nil {
		return nil, 0, err
	}

	n, err = msg.Decode(b)
	return msg, n, err
}

// readMessage() reads and copies a message from the buffer. The buffer bytes are
// committed as a result of the read.
func (this *service) readMessage(mtype message.MessageType, total int) (message.Message, int, error) {
	var (
		b   []byte
		err error
		n   int
		msg message.Message
	)

	if this.in == nil {
		err = ErrBufferNotReady
		return nil, 0, err
	}

	if len(this.intmp) < total {
		this.intmp = make([]byte, total)
	}

	// Read until we get total bytes
	l := 0
	for l < total {
		n, err = this.in.Read(this.intmp[l:])
		l += n
		glog.Debugf("read %d bytes, total %d", n, l)
		if err != nil {
			return nil, 0, err
		}
	}

	b = this.intmp[:total]

	msg, err = mtype.New()
	if err != nil {
		return msg, 0, err
	}

	n, err = msg.Decode(b)
	return msg, n, err
}

// writeMessage() writes a message to the outgoing buffer
func (this *service) writeMessage(msg message.Message) (int, error) {
	var (
		l    int = msg.Len()
		m, n int
		err  error
		buf  []byte
		wrap bool
	)

	if this.out == nil {
		return 0, ErrBufferNotReady
	}

	// This is to serialize writes to the underlying buffer. Multiple goroutines could
	// potentially get here because of calling Publish() or Subscribe() or other
	// functions that will send messages. For example, if a message is received in
	// another connetion, and the message needs to be published to this client, then
	// the Publish() function is called, and at the same time, another client could
	// do exactly the same thing.
	//
	// Not an ideal fix though. If possible we should remove mutex and be lockfree.
	// Mainly because when there's a large number of goroutines that want to publish
	// to this client, then they will all block. However, this will do for now.
	//
	// FIXME: Try to find a better way than a mutex...if possible.
	this.wmu.Lock()
	defer this.wmu.Unlock()

	buf, wrap, err = this.out.WriteWait(l)
	if err != nil {
		return 0, err
	}

	if wrap {
		if len(this.outtmp) < l {
			this.outtmp = make([]byte, l)
		}

		n, err = msg.Encode(this.outtmp[0:])
		if err != nil {
			return 0, err
		}

		m, err = this.out.Write(this.outtmp[0:n])
		if err != nil {
			return m, err
		}
	} else {
		n, err = msg.Encode(buf[0:])
		if err != nil {
			return 0, err
		}

		m, err = this.out.WriteCommit(n)
		if err != nil {
			return 0, err
		}
	}

	this.outStat.increment(int64(m))

	return m, nil
}
<|endoftext|>


<commit_before>// Copyright (c) 2014-2015, b3log.org
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Package session includes session related manipulations.
//
// Wide server side needs maintain two kinds of sessions:
//
//  1. HTTP session: mainly used for login authentication
//  2. Wide session: browser tab open/refresh will create one, and associates with HTTP session
//
// When a session gone: release all resources associated with it, such as running processes, event queues.
package session

import (
	"bytes"
	"encoding/json"
	"net/http"
	"os"
	"path/filepath"
	"sort"
	"strconv"
	"sync"
	"time"

	"github.com/b3log/wide/conf"
	"github.com/b3log/wide/event"
	"github.com/b3log/wide/log"
	"github.com/b3log/wide/util"
	"github.com/go-fsnotify/fsnotify"
	"github.com/gorilla/sessions"
	"github.com/gorilla/websocket"
)

const (
	sessionStateActive = iota
	sessionStateClosed // (not used so far)
)

// Logger.
var logger = log.NewLogger(os.Stdout)

var (
	// SessionWS holds all session channels. <sid, *util.WSChannel>
	SessionWS = map[string]*util.WSChannel{}

	// EditorWS holds all editor channels. <sid, *util.WSChannel>
	EditorWS = map[string]*util.WSChannel{}

	// OutputWS holds all output channels. <sid, *util.WSChannel>
	OutputWS = map[string]*util.WSChannel{}

	// NotificationWS holds all notification channels. <sid, *util.WSChannel>
	NotificationWS = map[string]*util.WSChannel{}

	// PlaygroundWS holds all playground channels. <sid, *util.WSChannel>
	PlaygroundWS = map[string]*util.WSChannel{}
)

// HTTP session store.
var HTTPSession = sessions.NewCookieStore([]byte("BEYOND"))

// WideSession represents a session associated with a browser tab.
type WideSession struct {
	ID          string                     // id
	Username    string                     // username
	HTTPSession *sessions.Session          // HTTP session related
	Processes   []*os.Process              // process set
	EventQueue  *event.UserEventQueue      // event queue
	State       int                        // state
	Content     *conf.LatestSessionContent // the latest session content
	FileWatcher *fsnotify.Watcher          // files change watcher
	Created     time.Time                  // create time
	Updated     time.Time                  // the latest use time
}

// Type of wide sessions.
type wSessions []*WideSession

// Wide sessions.
var WideSessions wSessions

// Exclusive lock.
var mutex sync.Mutex

// FixedTimeRelease releases invalid sessions.
//
// In some special cases (such as a browser uninterrupted refresh / refresh in the source code view) will occur
// some invalid sessions, the function checks and removes these invalid sessions periodically (1 hour).
//
// Invalid sessions: sessions that not used within 30 minutes, refers to WideSession.Updated field.
func FixedTimeRelease() {
	go func() {
		defer util.Recover()

		for _ = range time.Tick(time.Hour) {
			hour, _ := time.ParseDuration("-30m")
			threshold := time.Now().Add(hour)

			for _, s := range WideSessions {
				if s.Updated.Before(threshold) {
					logger.Debugf("Removes a invalid session [%s], user [%s]", s.ID, s.Username)

					WideSessions.Remove(s.ID)
				}
			}
		}
	}()
}

// Online user statistic report.
type userReport struct {
	username   string
	sessionCnt int
	processCnt int
	updated    time.Time
}

// report returns a online user statistics in pretty format.
func (u *userReport) report() string {
	return "[" + u.username + "] has [" + strconv.Itoa(u.sessionCnt) + "] sessions and [" + strconv.Itoa(u.processCnt) +
		"] running processes, latest activity [" + u.updated.Format("2006-01-02 15:04:05") + "]"
}

// FixedTimeReport reports the Wide sessions status periodically (10 minutes).
func FixedTimeReport() {
	go func() {
		defer util.Recover()

		for _ = range time.Tick(10 * time.Minute) {
			users := userReports{}
			processSum := 0

			for _, s := range WideSessions {
				processCnt := len(s.Processes)
				processSum += processCnt

				if report, exists := contains(users, s.Username); exists {
					if s.Updated.After(report.updated) {
						report.updated = s.Updated
					}

					report.sessionCnt++
					report.processCnt += processCnt
				} else {
					users = append(users, &userReport{username: s.Username, sessionCnt: 1, processCnt: processCnt, updated: s.Updated})
				}
			}

			var buf bytes.Buffer
			buf.WriteString("\n  [" + strconv.Itoa(len(users)) + "] users, [" + strconv.Itoa(processSum) + "] running processes and [" +
				strconv.Itoa(len(WideSessions)) + "] sessions currently\n")

			sort.Sort(users)

			for _, t := range users {
				buf.WriteString("    " + t.report() + "\n")
			}

			logger.Info(buf.String())
		}
	}()
}

func contains(reports []*userReport, username string) (*userReport, bool) {
	for _, ur := range reports {
		if username == ur.username {
			return ur, true
		}
	}

	return nil, false
}

type userReports []*userReport

func (f userReports) Len() int           { return len(f) }
func (f userReports) Swap(i, j int)      { f[i], f[j] = f[j], f[i] }
func (f userReports) Less(i, j int) bool { return f[i].processCnt > f[j].processCnt }

// WSHandler handles request of creating session channel.
//
// When a channel closed, releases all resources associated with it.
func WSHandler(w http.ResponseWriter, r *http.Request) {
	sid := r.URL.Query()["sid"][0]
	wSession := WideSessions.Get(sid)
	if nil == wSession {
		httpSession, _ := HTTPSession.Get(r, "wide-session")

		if httpSession.IsNew {
			return
		}

		httpSession.Options.MaxAge = conf.Wide.HTTPSessionMaxAge
		httpSession.Save(r, w)

		wSession = WideSessions.New(httpSession, sid)

		logger.Tracef("Created a wide session [%s] for websocket reconnecting, user [%s]", sid, wSession.Username)
	}

	conn, _ := websocket.Upgrade(w, r, nil, 1024, 1024)
	wsChan := util.WSChannel{Sid: sid, Conn: conn, Request: r, Time: time.Now()}

	ret := map[string]interface{}{"output": "Session initialized", "cmd": "init-session"}
	err := wsChan.WriteJSON(&ret)
	if nil != err {
		return
	}

	SessionWS[sid] = &wsChan

	logger.Tracef("Open a new [Session Channel] with session [%s], %d", sid, len(SessionWS))

	input := map[string]interface{}{}

	for {
		if err := wsChan.ReadJSON(&input); err != nil {
			logger.Tracef("[Session Channel] of session [%s] disconnected, releases all resources with it, user [%s]", sid, wSession.Username)

			WideSessions.Remove(sid)

			return
		}

		ret = map[string]interface{}{"output": "", "cmd": "session-output"}

		if err := wsChan.WriteJSON(&ret); err != nil {
			logger.Error("Session WS ERROR: " + err.Error())

			return
		}

		wsChan.Time = time.Now()
	}
}

// SaveContentHandler handles request of session content string.
func SaveContentHandler(w http.ResponseWriter, r *http.Request) {
	data := map[string]interface{}{"succ": true}
	defer util.RetJSON(w, r, data)

	args := struct {
		Sid string
		*conf.LatestSessionContent
	}{}

	if err := json.NewDecoder(r.Body).Decode(&args); err != nil {
		logger.Error(err)
		data["succ"] = false

		return
	}

	wSession := WideSessions.Get(args.Sid)
	if nil == wSession {
		data["succ"] = false

		return
	}

	wSession.Content = args.LatestSessionContent

	for _, user := range conf.Users {
		if user.Name == wSession.Username {
			// update the variable in-memory, session.FixedTimeSave() function will persist it periodically
			user.LatestSessionContent = wSession.Content

			user.Lived = time.Now().UnixNano()

			wSession.Refresh()

			return
		}
	}
}

// SetProcesses binds process set with the wide session.
func (s *WideSession) SetProcesses(ps []*os.Process) {
	s.Processes = ps

	s.Refresh()
}

// Refresh refreshes the channel by updating its use time.
func (s *WideSession) Refresh() {
	s.Updated = time.Now()
}

// New creates a wide session.
func (sessions *wSessions) New(httpSession *sessions.Session, sid string) *WideSession {
	mutex.Lock()
	defer mutex.Unlock()

	now := time.Now()

	// create user event queue
	userEventQueue := event.UserEventQueues.New(sid)

	username := httpSession.Values["username"].(string)

	ret := &WideSession{
		ID:          sid,
		Username:    username,
		HTTPSession: httpSession,
		EventQueue:  userEventQueue,
		State:       sessionStateActive,
		Content:     &conf.LatestSessionContent{},
		Created:     now,
		Updated:     now,
	}

	*sessions = append(*sessions, ret)

	if "playground" == username {
		return ret
	}

	// add a filesystem watcher to notify front-end after the files changed
	watcher, err := fsnotify.NewWatcher()

	if err != nil {
		logger.Error(err)

		return ret
	}

	go func() {
		defer util.Recover()

		workspaces := filepath.SplitList(conf.GetUserWorkspace(username))
		for _, workspace := range workspaces {
			filepath.Walk(filepath.Join(workspace, "src"), func(dirPath string, f os.FileInfo, err error) error {
				if ".git" == f.Name() { // XXX: discard other unconcered dirs
					return filepath.SkipDir
				}

				if f.IsDir() {
					if err = watcher.Add(dirPath); nil != err {
						logger.Error(err, dirPath)
					}

					logger.Tracef("Added a file watcher [%s]", dirPath)
				}

				return nil
			})

		}

		ret.FileWatcher = watcher
	}()

	go func() {
		defer util.Recover()

		for {
			ch := SessionWS[sid]
			if nil == ch {
				return // release this gorutine
			}

			select {
			case event := <-watcher.Events:
				path := event.Name
				dir := filepath.Dir(path)

				ch = SessionWS[sid]
				if nil == ch {
					return // release this gorutine
				}

				if event.Op&fsnotify.Create == fsnotify.Create {
					if err = watcher.Add(path); nil != err {
						logger.Warn(err, path)
					}

					logger.Tracef("Added a file watcher [%s]", path)

					cmd := map[string]interface{}{"path": path, "dir": dir, "cmd": "create-file"}
					ch.WriteJSON(&cmd)
				} else if event.Op&fsnotify.Remove == fsnotify.Remove {
					cmd := map[string]interface{}{"path": path, "dir": dir, "cmd": "remove-file"}
					ch.WriteJSON(&cmd)

				} else if event.Op&fsnotify.Rename == fsnotify.Rename {
					cmd := map[string]interface{}{"path": path, "dir": dir, "cmd": "rename-file"}
					ch.WriteJSON(&cmd)
				}
			case err := <-watcher.Errors:
				if nil != err {
					logger.Error("File watcher ERROR: ", err)
				}
			}
		}
	}()

	return ret
}

// Get gets a wide session with the specified session id.
func (sessions *wSessions) Get(sid string) *WideSession {
	mutex.Lock()
	defer mutex.Unlock()

	for _, s := range *sessions {
		if s.ID == sid {
			return s
		}
	}

	return nil
}

// Remove removes a wide session specified with the given session id, releases resources associated with it.
//
// Session-related resources:
//
//  1. user event queue
//  2. process set
//  3. websocket channels
//  4. file watcher
func (sessions *wSessions) Remove(sid string) {
	mutex.Lock()
	defer mutex.Unlock()

	for i, s := range *sessions {
		if s.ID == sid {
			// remove from session set
			*sessions = append((*sessions)[:i], (*sessions)[i+1:]...)

			// close user event queue
			event.UserEventQueues.Close(sid)

			// kill processes
			for _, p := range s.Processes {
				if err := p.Kill(); nil != err {
					logger.Errorf("Can't kill process [%d] of session [%s], user [%s]", p.Pid, sid, s.Username)
				} else {
					logger.Debugf("Killed a process [%d] of session [%s], user [%s]", p.Pid, sid, s.Username)
				}
			}

			// close websocket channels
			if ws, ok := OutputWS[sid]; ok {
				ws.Close()
				delete(OutputWS, sid)
			}

			if ws, ok := NotificationWS[sid]; ok {
				ws.Close()
				delete(NotificationWS, sid)
			}

			if ws, ok := SessionWS[sid]; ok {
				ws.Close()
				delete(SessionWS, sid)
			}

			if ws, ok := PlaygroundWS[sid]; ok {
				ws.Close()
				delete(PlaygroundWS, sid)
			}

			// file watcher
			if nil != s.FileWatcher {
				s.FileWatcher.Close()
			}

			cnt := 0 // count wide sessions associated with HTTP session
			for _, ses := range *sessions {
				if ses.Username == s.Username {
					cnt++
				}
			}

			logger.Debugf("Removed a session [%s] of user [%s], it has [%d] sessions currently", sid, s.Username, cnt)

			return
		}
	}
}

// GetByUsername gets wide sessions.
func (sessions *wSessions) GetByUsername(username string) []*WideSession {
	mutex.Lock()
	defer mutex.Unlock()

	ret := []*WideSession{}

	for _, s := range *sessions {
		if s.Username == username {
			ret = append(ret, s)
		}
	}

	return ret
}
<commit_msg>reduce gorutines<commit_after>// Copyright (c) 2014-2015, b3log.org
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Package session includes session related manipulations.
//
// Wide server side needs maintain two kinds of sessions:
//
//  1. HTTP session: mainly used for login authentication
//  2. Wide session: browser tab open/refresh will create one, and associates with HTTP session
//
// When a session gone: release all resources associated with it, such as running processes, event queues.
package session

import (
	"bytes"
	"encoding/json"
	"net/http"
	"os"
	"path/filepath"
	"sort"
	"strconv"
	"sync"
	"time"

	"github.com/b3log/wide/conf"
	"github.com/b3log/wide/event"
	"github.com/b3log/wide/log"
	"github.com/b3log/wide/util"
	"github.com/go-fsnotify/fsnotify"
	"github.com/gorilla/sessions"
	"github.com/gorilla/websocket"
)

const (
	sessionStateActive = iota
	sessionStateClosed // (not used so far)
)

// Logger.
var logger = log.NewLogger(os.Stdout)

var (
	// SessionWS holds all session channels. <sid, *util.WSChannel>
	SessionWS = map[string]*util.WSChannel{}

	// EditorWS holds all editor channels. <sid, *util.WSChannel>
	EditorWS = map[string]*util.WSChannel{}

	// OutputWS holds all output channels. <sid, *util.WSChannel>
	OutputWS = map[string]*util.WSChannel{}

	// NotificationWS holds all notification channels. <sid, *util.WSChannel>
	NotificationWS = map[string]*util.WSChannel{}

	// PlaygroundWS holds all playground channels. <sid, *util.WSChannel>
	PlaygroundWS = map[string]*util.WSChannel{}
)

// HTTP session store.
var HTTPSession = sessions.NewCookieStore([]byte("BEYOND"))

// WideSession represents a session associated with a browser tab.
type WideSession struct {
	ID          string                     // id
	Username    string                     // username
	HTTPSession *sessions.Session          // HTTP session related
	Processes   []*os.Process              // process set
	EventQueue  *event.UserEventQueue      // event queue
	State       int                        // state
	Content     *conf.LatestSessionContent // the latest session content
	FileWatcher *fsnotify.Watcher          // files change watcher
	Created     time.Time                  // create time
	Updated     time.Time                  // the latest use time
}

// Type of wide sessions.
type wSessions []*WideSession

// Wide sessions.
var WideSessions wSessions

// Exclusive lock.
var mutex sync.Mutex

// FixedTimeRelease releases invalid sessions.
//
// In some special cases (such as a browser uninterrupted refresh / refresh in the source code view) will occur
// some invalid sessions, the function checks and removes these invalid sessions periodically (1 hour).
//
// Invalid sessions: sessions that not used within 30 minutes, refers to WideSession.Updated field.
func FixedTimeRelease() {
	go func() {
		defer util.Recover()

		for _ = range time.Tick(time.Hour) {
			hour, _ := time.ParseDuration("-30m")
			threshold := time.Now().Add(hour)

			for _, s := range WideSessions {
				if s.Updated.Before(threshold) {
					logger.Debugf("Removes a invalid session [%s], user [%s]", s.ID, s.Username)

					WideSessions.Remove(s.ID)
				}
			}
		}
	}()
}

// Online user statistic report.
type userReport struct {
	username   string
	sessionCnt int
	processCnt int
	updated    time.Time
}

// report returns a online user statistics in pretty format.
func (u *userReport) report() string {
	return "[" + u.username + "] has [" + strconv.Itoa(u.sessionCnt) + "] sessions and [" + strconv.Itoa(u.processCnt) +
		"] running processes, latest activity [" + u.updated.Format("2006-01-02 15:04:05") + "]"
}

// FixedTimeReport reports the Wide sessions status periodically (10 minutes).
func FixedTimeReport() {
	go func() {
		defer util.Recover()

		for _ = range time.Tick(10 * time.Minute) {
			users := userReports{}
			processSum := 0

			for _, s := range WideSessions {
				processCnt := len(s.Processes)
				processSum += processCnt

				if report, exists := contains(users, s.Username); exists {
					if s.Updated.After(report.updated) {
						report.updated = s.Updated
					}

					report.sessionCnt++
					report.processCnt += processCnt
				} else {
					users = append(users, &userReport{username: s.Username, sessionCnt: 1, processCnt: processCnt, updated: s.Updated})
				}
			}

			var buf bytes.Buffer
			buf.WriteString("\n  [" + strconv.Itoa(len(users)) + "] users, [" + strconv.Itoa(processSum) + "] running processes and [" +
				strconv.Itoa(len(WideSessions)) + "] sessions currently\n")

			sort.Sort(users)

			for _, t := range users {
				buf.WriteString("    " + t.report() + "\n")
			}

			logger.Info(buf.String())
		}
	}()
}

func contains(reports []*userReport, username string) (*userReport, bool) {
	for _, ur := range reports {
		if username == ur.username {
			return ur, true
		}
	}

	return nil, false
}

type userReports []*userReport

func (f userReports) Len() int           { return len(f) }
func (f userReports) Swap(i, j int)      { f[i], f[j] = f[j], f[i] }
func (f userReports) Less(i, j int) bool { return f[i].processCnt > f[j].processCnt }

// WSHandler handles request of creating session channel.
//
// When a channel closed, releases all resources associated with it.
func WSHandler(w http.ResponseWriter, r *http.Request) {
	sid := r.URL.Query()["sid"][0]
	wSession := WideSessions.Get(sid)
	if nil == wSession {
		httpSession, _ := HTTPSession.Get(r, "wide-session")

		if httpSession.IsNew {
			return
		}

		httpSession.Options.MaxAge = conf.Wide.HTTPSessionMaxAge
		httpSession.Save(r, w)

		wSession = WideSessions.New(httpSession, sid)

		logger.Tracef("Created a wide session [%s] for websocket reconnecting, user [%s]", sid, wSession.Username)
	}

	conn, _ := websocket.Upgrade(w, r, nil, 1024, 1024)
	wsChan := util.WSChannel{Sid: sid, Conn: conn, Request: r, Time: time.Now()}

	ret := map[string]interface{}{"output": "Session initialized", "cmd": "init-session"}
	err := wsChan.WriteJSON(&ret)
	if nil != err {
		return
	}

	SessionWS[sid] = &wsChan

	logger.Tracef("Open a new [Session Channel] with session [%s], %d", sid, len(SessionWS))

	input := map[string]interface{}{}

	for {
		if err := wsChan.ReadJSON(&input); err != nil {
			logger.Tracef("[Session Channel] of session [%s] disconnected, releases all resources with it, user [%s]", sid, wSession.Username)

			WideSessions.Remove(sid)

			return
		}

		ret = map[string]interface{}{"output": "", "cmd": "session-output"}

		if err := wsChan.WriteJSON(&ret); err != nil {
			logger.Error("Session WS ERROR: " + err.Error())

			return
		}

		wsChan.Time = time.Now()
	}
}

// SaveContentHandler handles request of session content string.
func SaveContentHandler(w http.ResponseWriter, r *http.Request) {
	data := map[string]interface{}{"succ": true}
	defer util.RetJSON(w, r, data)

	args := struct {
		Sid string
		*conf.LatestSessionContent
	}{}

	if err := json.NewDecoder(r.Body).Decode(&args); err != nil {
		logger.Error(err)
		data["succ"] = false

		return
	}

	wSession := WideSessions.Get(args.Sid)
	if nil == wSession {
		data["succ"] = false

		return
	}

	wSession.Content = args.LatestSessionContent

	for _, user := range conf.Users {
		if user.Name == wSession.Username {
			// update the variable in-memory, session.FixedTimeSave() function will persist it periodically
			user.LatestSessionContent = wSession.Content

			user.Lived = time.Now().UnixNano()

			wSession.Refresh()

			return
		}
	}
}

// SetProcesses binds process set with the wide session.
func (s *WideSession) SetProcesses(ps []*os.Process) {
	s.Processes = ps

	s.Refresh()
}

// Refresh refreshes the channel by updating its use time.
func (s *WideSession) Refresh() {
	s.Updated = time.Now()
}

// New creates a wide session.
func (sessions *wSessions) New(httpSession *sessions.Session, sid string) *WideSession {
	mutex.Lock()
	defer mutex.Unlock()

	username := httpSession.Values["username"].(string)
	now := time.Now()

	ret := &WideSession{
		ID:          sid,
		Username:    username,
		HTTPSession: httpSession,
		EventQueue:  nil,
		State:       sessionStateActive,
		Content:     &conf.LatestSessionContent{},
		Created:     now,
		Updated:     now,
	}

	*sessions = append(*sessions, ret)

	if "playground" == username {
		return ret
	}

	// create user event queue
	ret.EventQueue = event.UserEventQueues.New(sid)

	// add a filesystem watcher to notify front-end after the files changed
	watcher, err := fsnotify.NewWatcher()
	if err != nil {
		logger.Error(err)

		return ret
	}

	go func() {
		defer util.Recover()

		workspaces := filepath.SplitList(conf.GetUserWorkspace(username))
		for _, workspace := range workspaces {
			filepath.Walk(filepath.Join(workspace, "src"), func(dirPath string, f os.FileInfo, err error) error {
				if ".git" == f.Name() { // XXX: discard other unconcered dirs
					return filepath.SkipDir
				}

				if f.IsDir() {
					if err = watcher.Add(dirPath); nil != err {
						logger.Error(err, dirPath)
					}

					logger.Tracef("Added a file watcher [%s]", dirPath)
				}

				return nil
			})

		}

		ret.FileWatcher = watcher
	}()

	go func() {
		defer util.Recover()

		for {
			ch := SessionWS[sid]
			if nil == ch {
				return // release this gorutine
			}

			select {
			case event := <-watcher.Events:
				path := event.Name
				dir := filepath.Dir(path)

				ch = SessionWS[sid]
				if nil == ch {
					return // release this gorutine
				}

				if event.Op&fsnotify.Create == fsnotify.Create {
					if err = watcher.Add(path); nil != err {
						logger.Warn(err, path)
					}

					logger.Tracef("Added a file watcher [%s]", path)

					cmd := map[string]interface{}{"path": path, "dir": dir, "cmd": "create-file"}
					ch.WriteJSON(&cmd)
				} else if event.Op&fsnotify.Remove == fsnotify.Remove {
					cmd := map[string]interface{}{"path": path, "dir": dir, "cmd": "remove-file"}
					ch.WriteJSON(&cmd)

				} else if event.Op&fsnotify.Rename == fsnotify.Rename {
					cmd := map[string]interface{}{"path": path, "dir": dir, "cmd": "rename-file"}
					ch.WriteJSON(&cmd)
				}
			case err := <-watcher.Errors:
				if nil != err {
					logger.Error("File watcher ERROR: ", err)
				}
			}
		}
	}()

	return ret
}

// Get gets a wide session with the specified session id.
func (sessions *wSessions) Get(sid string) *WideSession {
	mutex.Lock()
	defer mutex.Unlock()

	for _, s := range *sessions {
		if s.ID == sid {
			return s
		}
	}

	return nil
}

// Remove removes a wide session specified with the given session id, releases resources associated with it.
//
// Session-related resources:
//
//  1. user event queue
//  2. process set
//  3. websocket channels
//  4. file watcher
func (sessions *wSessions) Remove(sid string) {
	mutex.Lock()
	defer mutex.Unlock()

	for i, s := range *sessions {
		if s.ID == sid {
			// remove from session set
			*sessions = append((*sessions)[:i], (*sessions)[i+1:]...)

			// close user event queue
			event.UserEventQueues.Close(sid)

			// kill processes
			for _, p := range s.Processes {
				if err := p.Kill(); nil != err {
					logger.Errorf("Can't kill process [%d] of session [%s], user [%s]", p.Pid, sid, s.Username)
				} else {
					logger.Debugf("Killed a process [%d] of session [%s], user [%s]", p.Pid, sid, s.Username)
				}
			}

			// close websocket channels
			if ws, ok := OutputWS[sid]; ok {
				ws.Close()
				delete(OutputWS, sid)
			}

			if ws, ok := NotificationWS[sid]; ok {
				ws.Close()
				delete(NotificationWS, sid)
			}

			if ws, ok := SessionWS[sid]; ok {
				ws.Close()
				delete(SessionWS, sid)
			}

			if ws, ok := PlaygroundWS[sid]; ok {
				ws.Close()
				delete(PlaygroundWS, sid)
			}

			// file watcher
			if nil != s.FileWatcher {
				s.FileWatcher.Close()
			}

			cnt := 0 // count wide sessions associated with HTTP session
			for _, ses := range *sessions {
				if ses.Username == s.Username {
					cnt++
				}
			}

			logger.Debugf("Removed a session [%s] of user [%s], it has [%d] sessions currently", sid, s.Username, cnt)

			return
		}
	}
}

// GetByUsername gets wide sessions.
func (sessions *wSessions) GetByUsername(username string) []*WideSession {
	mutex.Lock()
	defer mutex.Unlock()

	ret := []*WideSession{}

	for _, s := range *sessions {
		if s.Username == username {
			ret = append(ret, s)
		}
	}

	return ret
}
<|endoftext|>


<commit_before>package rest

import (
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"testing"

	"github.com/bitfinexcom/bitfinex-api-go/pkg/models/pulse"
	"github.com/bitfinexcom/bitfinex-api-go/pkg/models/pulseprofile"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestPublicPulseProfile(t *testing.T) {
	t.Run("missing arguments", func(t *testing.T) {
		c := NewClient()
		pp, err := c.Pulse.PublicPulseProfile("")
		require.NotNil(t, err)
		require.Nil(t, pp)
	})

	t.Run("response data slice too short", func(t *testing.T) {
		handler := func(w http.ResponseWriter, r *http.Request) {
			respMock := []interface{}{"abc123"}
			payload, _ := json.Marshal(respMock)
			w.Write(payload)
		}

		server := httptest.NewServer(http.HandlerFunc(handler))
		defer server.Close()

		c := NewClientWithURL(server.URL)
		pp, err := c.Pulse.PublicPulseProfile("Bitfinex")
		require.NotNil(t, err)
		require.Nil(t, pp)
	})

	t.Run("valid response data", func(t *testing.T) {
		handler := func(w http.ResponseWriter, r *http.Request) {
			assert.Equal(t, "/pulse/profile/Bitfinex", r.RequestURI)
			respMock := []interface{}{
				"abc123",
				float64(1591614631576),
				nil,
				"nickname",
				nil,
				"picture",
				"text",
				nil,
				nil,
				"twitter",
				nil,
				30,
				5,
				nil,
			}
			payload, _ := json.Marshal(respMock)
			w.Write(payload)
		}

		server := httptest.NewServer(http.HandlerFunc(handler))
		defer server.Close()

		c := NewClientWithURL(server.URL)
		pp, err := c.Pulse.PublicPulseProfile("Bitfinex")
		require.Nil(t, err)

		expected := &pulseprofile.PulseProfile{
			ID:            "abc123",
			MTS:           1591614631576,
			Nickname:      "nickname",
			Picture:       "picture",
			Text:          "text",
			TwitterHandle: "twitter",
		}
		assert.Equal(t, expected, pp)
	})
}

func TestPublicPulseHistory(t *testing.T) {
	t.Run("response data slice too short", func(t *testing.T) {
		handler := func(w http.ResponseWriter, r *http.Request) {
			respMock := []interface{}{
				[]interface{}{"id"},
			}
			payload, _ := json.Marshal(respMock)
			w.Write(payload)
		}

		server := httptest.NewServer(http.HandlerFunc(handler))
		defer server.Close()

		c := NewClientWithURL(server.URL)
		pp, err := c.Pulse.PublicPulseHistory(1, 0)
		require.NotNil(t, err)
		require.Nil(t, pp)
	})

	t.Run("valid response data no profile", func(t *testing.T) {
		handler := func(w http.ResponseWriter, r *http.Request) {
			assert.Equal(t, "/pulse/hist?end=1591691528075&limit=1", r.RequestURI)
			limit := r.URL.Query().Get("limit")
			end := r.URL.Query().Get("end")
			assert.Equal(t, "1", limit)
			assert.Equal(t, "1591691528075", end)

			respMock := []interface{}{
				[]interface{}{
					"id",
					float64(1591614631576),
					nil,
					"uid",
					nil,
					"title",
					"content",
					nil,
					nil,
					1,
					1,
					nil,
					[]interface{}{"tag1", "tag2"},
					[]interface{}{"attach1", "attach2"},
					nil,
					5,
					nil,
					nil,
					nil,
				},
			}
			payload, _ := json.Marshal(respMock)
			w.Write(payload)
		}

		server := httptest.NewServer(http.HandlerFunc(handler))
		defer server.Close()

		c := NewClientWithURL(server.URL)
		pph, err := c.Pulse.PublicPulseHistory(1, 1591691528075)
		require.Nil(t, err)

		expected := &pulse.Pulse{
			ID:          "id",
			MTS:         1591614631576,
			UserID:      "uid",
			Title:       "title",
			Content:     "content",
			IsPin:       1,
			IsPublic:    1,
			Tags:        []string{"tag1", "tag2"},
			Attachments: []string{"attach1", "attach2"},
			Likes:       5,
		}

		assert.Equal(t, expected, pph[0])
	})
}

func TestAddPulse(t *testing.T) {
	t.Run("invalid payload", func(t *testing.T) {
		p := &pulse.Pulse{Title: "foo"}
		c := NewClient()
		pm, err := c.Pulse.AddPulse(p)
		require.NotNil(t, err)
		require.Nil(t, pm)
	})

	t.Run("response data slice too short", func(t *testing.T) {
		handler := func(w http.ResponseWriter, r *http.Request) {
			assert.Equal(t, "/auth/w/pulse/add", r.RequestURI)
			respMock := []interface{}{"id"}
			payload, _ := json.Marshal(respMock)
			w.Write(payload)
		}

		server := httptest.NewServer(http.HandlerFunc(handler))
		defer server.Close()

		c := NewClientWithURL(server.URL)
		pm, err := c.Pulse.AddPulse(&pulse.Pulse{Title: "foo bar baz qux 123"})
		require.NotNil(t, err)
		require.Nil(t, pm)
	})

	t.Run("valid payload", func(t *testing.T) {
		handler := func(w http.ResponseWriter, r *http.Request) {
			respMock := []interface{}{
				"id",
				float64(1591614631576),
				nil,
				"uid",
				nil,
				"title",
				"content",
				nil,
				nil,
				1,
				1,
				nil,
				[]interface{}{"tag1", "tag2"},
				[]interface{}{"attach1", "attach2"},
				nil,
				5,
				nil,
				nil,
				nil,
			}
			payload, _ := json.Marshal(respMock)
			w.Write(payload)
		}

		server := httptest.NewServer(http.HandlerFunc(handler))
		defer server.Close()

		c := NewClientWithURL(server.URL)
		pm, err := c.Pulse.AddPulse(&pulse.Pulse{Title: "foo bar baz qux 123"})
		require.Nil(t, err)

		expected := &pulse.Pulse{
			ID:          "id",
			MTS:         1591614631576,
			UserID:      "uid",
			Title:       "title",
			Content:     "content",
			IsPin:       1,
			IsPublic:    1,
			Tags:        []string{"tag1", "tag2"},
			Attachments: []string{"attach1", "attach2"},
			Likes:       5,
		}

		assert.Equal(t, expected, pm)
	})
}

func TestPulseHistory(t *testing.T) {
	t.Run("response data slice too short", func(t *testing.T) {
		handler := func(w http.ResponseWriter, r *http.Request) {
			assert.Equal(t, "/auth/r/pulse/hist?isPublic=0", r.RequestURI)
			isPublic := r.URL.Query().Get("isPublic")
			assert.Equal(t, "0", isPublic)

			respMock := []interface{}{
				[]interface{}{"id"},
			}
			payload, _ := json.Marshal(respMock)
			w.Write(payload)
		}

		server := httptest.NewServer(http.HandlerFunc(handler))
		defer server.Close()

		c := NewClientWithURL(server.URL)
		pp, err := c.Pulse.PulseHistory(0)
		require.NotNil(t, err)
		require.Nil(t, pp)
	})

	t.Run("isPublic 0", func(t *testing.T) {
		handler := func(w http.ResponseWriter, r *http.Request) {
			isPublic := r.URL.Query().Get("isPublic")
			assert.Equal(t, "0", isPublic)

			respMock := []interface{}{
				[]interface{}{
					"id",
					float64(1591614631576),
					nil,
					"uid",
					nil,
					"title",
					"content",
					nil,
					nil,
					1,
					1,
					nil,
					[]interface{}{"tag1", "tag2"},
					[]interface{}{"attach1", "attach2"},
					nil,
					5,
					nil,
					nil,
					nil,
				},
			}
			payload, _ := json.Marshal(respMock)
			w.Write(payload)
		}

		server := httptest.NewServer(http.HandlerFunc(handler))
		defer server.Close()

		c := NewClientWithURL(server.URL)
		pm, err := c.Pulse.PulseHistory(0)
		require.Nil(t, err)

		expected := &pulse.Pulse{
			ID:          "id",
			MTS:         1591614631576,
			UserID:      "uid",
			Title:       "title",
			Content:     "content",
			IsPin:       1,
			IsPublic:    1,
			Tags:        []string{"tag1", "tag2"},
			Attachments: []string{"attach1", "attach2"},
			Likes:       5,
		}

		assert.Equal(t, expected, pm[0])
	})

	t.Run("isPublic 1", func(t *testing.T) {
		handler := func(w http.ResponseWriter, r *http.Request) {
			isPublic := r.URL.Query().Get("isPublic")
			assert.Equal(t, "1", isPublic)

			respMock := []interface{}{
				[]interface{}{
					"id",
					float64(1591614631576),
					nil,
					"uid",
					nil,
					"title",
					"content",
					nil,
					nil,
					1,
					1,
					nil,
					[]interface{}{"tag1", "tag2"},
					[]interface{}{"attach1", "attach2"},
					nil,
					5,
					nil,
					nil,
					[]interface{}{
						[]interface{}{
							"abc123",
							float64(1591614631576),
							nil,
							"nickname",
							nil,
							"picture",
							"text",
							nil,
							nil,
							"twitter",
							nil,
							30,
							5,
							nil,
						},
					},
				},
			}
			payload, _ := json.Marshal(respMock)
			w.Write(payload)
		}

		server := httptest.NewServer(http.HandlerFunc(handler))
		defer server.Close()

		c := NewClientWithURL(server.URL)
		pm, err := c.Pulse.PulseHistory(1)
		require.Nil(t, err)

		expected := &pulse.Pulse{
			ID:          "id",
			MTS:         1591614631576,
			UserID:      "uid",
			Title:       "title",
			Content:     "content",
			IsPin:       1,
			IsPublic:    1,
			Tags:        []string{"tag1", "tag2"},
			Attachments: []string{"attach1", "attach2"},
			Likes:       5,
			PulseProfile: &pulseprofile.PulseProfile{
				ID:            "abc123",
				MTS:           1591614631576,
				Nickname:      "nickname",
				Picture:       "picture",
				Text:          "text",
				TwitterHandle: "twitter",
			},
		}

		assert.Equal(t, expected, pm[0])
	})
}

func TestDeletePulse(t *testing.T) {
	t.Run("response", func(t *testing.T) {
		handler := func(w http.ResponseWriter, r *http.Request) {
			assert.Equal(t, "/auth/w/pulse/del", r.RequestURI)

			respMock := []interface{}{1}
			payload, _ := json.Marshal(respMock)
			w.Write(payload)
		}

		server := httptest.NewServer(http.HandlerFunc(handler))
		defer server.Close()

		c := NewClientWithURL(server.URL)
		deleted, err := c.Pulse.DeletePulse("abc123")
		require.Nil(t, err)
		assert.Equal(t, 1, deleted)
	})
}
<commit_msg>fixed linting issues<commit_after>package rest

import (
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"testing"

	"github.com/bitfinexcom/bitfinex-api-go/pkg/models/pulse"
	"github.com/bitfinexcom/bitfinex-api-go/pkg/models/pulseprofile"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestPublicPulseProfile(t *testing.T) {
	t.Run("missing arguments", func(t *testing.T) {
		c := NewClient()
		pp, err := c.Pulse.PublicPulseProfile("")
		require.NotNil(t, err)
		require.Nil(t, pp)
	})

	t.Run("response data slice too short", func(t *testing.T) {
		handler := func(w http.ResponseWriter, r *http.Request) {
			respMock := []interface{}{"abc123"}
			payload, _ := json.Marshal(respMock)
			_, err := w.Write(payload)
			require.Nil(t, err)
		}

		server := httptest.NewServer(http.HandlerFunc(handler))
		defer server.Close()

		c := NewClientWithURL(server.URL)
		pp, err := c.Pulse.PublicPulseProfile("Bitfinex")
		require.NotNil(t, err)
		require.Nil(t, pp)
	})

	t.Run("valid response data", func(t *testing.T) {
		handler := func(w http.ResponseWriter, r *http.Request) {
			assert.Equal(t, "/pulse/profile/Bitfinex", r.RequestURI)
			respMock := []interface{}{
				"abc123",
				float64(1591614631576),
				nil,
				"nickname",
				nil,
				"picture",
				"text",
				nil,
				nil,
				"twitter",
				nil,
				30,
				5,
				nil,
			}
			payload, _ := json.Marshal(respMock)
			_, err := w.Write(payload)
			require.Nil(t, err)
		}

		server := httptest.NewServer(http.HandlerFunc(handler))
		defer server.Close()

		c := NewClientWithURL(server.URL)
		pp, err := c.Pulse.PublicPulseProfile("Bitfinex")
		require.Nil(t, err)

		expected := &pulseprofile.PulseProfile{
			ID:            "abc123",
			MTS:           1591614631576,
			Nickname:      "nickname",
			Picture:       "picture",
			Text:          "text",
			TwitterHandle: "twitter",
		}
		assert.Equal(t, expected, pp)
	})
}

func TestPublicPulseHistory(t *testing.T) {
	t.Run("response data slice too short", func(t *testing.T) {
		handler := func(w http.ResponseWriter, r *http.Request) {
			respMock := []interface{}{
				[]interface{}{"id"},
			}
			payload, _ := json.Marshal(respMock)
			_, err := w.Write(payload)
			require.Nil(t, err)
		}

		server := httptest.NewServer(http.HandlerFunc(handler))
		defer server.Close()

		c := NewClientWithURL(server.URL)
		pp, err := c.Pulse.PublicPulseHistory(1, 0)
		require.NotNil(t, err)
		require.Nil(t, pp)
	})

	t.Run("valid response data no profile", func(t *testing.T) {
		handler := func(w http.ResponseWriter, r *http.Request) {
			assert.Equal(t, "/pulse/hist?end=1591691528075&limit=1", r.RequestURI)
			limit := r.URL.Query().Get("limit")
			end := r.URL.Query().Get("end")
			assert.Equal(t, "1", limit)
			assert.Equal(t, "1591691528075", end)

			respMock := []interface{}{
				[]interface{}{
					"id",
					float64(1591614631576),
					nil,
					"uid",
					nil,
					"title",
					"content",
					nil,
					nil,
					1,
					1,
					nil,
					[]interface{}{"tag1", "tag2"},
					[]interface{}{"attach1", "attach2"},
					nil,
					5,
					nil,
					nil,
					nil,
				},
			}
			payload, _ := json.Marshal(respMock)
			_, err := w.Write(payload)
			require.Nil(t, err)
		}

		server := httptest.NewServer(http.HandlerFunc(handler))
		defer server.Close()

		c := NewClientWithURL(server.URL)
		pph, err := c.Pulse.PublicPulseHistory(1, 1591691528075)
		require.Nil(t, err)

		expected := &pulse.Pulse{
			ID:          "id",
			MTS:         1591614631576,
			UserID:      "uid",
			Title:       "title",
			Content:     "content",
			IsPin:       1,
			IsPublic:    1,
			Tags:        []string{"tag1", "tag2"},
			Attachments: []string{"attach1", "attach2"},
			Likes:       5,
		}

		assert.Equal(t, expected, pph[0])
	})
}

func TestAddPulse(t *testing.T) {
	t.Run("invalid payload", func(t *testing.T) {
		p := &pulse.Pulse{Title: "foo"}
		c := NewClient()
		pm, err := c.Pulse.AddPulse(p)
		require.NotNil(t, err)
		require.Nil(t, pm)
	})

	t.Run("response data slice too short", func(t *testing.T) {
		handler := func(w http.ResponseWriter, r *http.Request) {
			assert.Equal(t, "/auth/w/pulse/add", r.RequestURI)
			respMock := []interface{}{"id"}
			payload, _ := json.Marshal(respMock)
			_, err := w.Write(payload)
			require.Nil(t, err)
		}

		server := httptest.NewServer(http.HandlerFunc(handler))
		defer server.Close()

		c := NewClientWithURL(server.URL)
		pm, err := c.Pulse.AddPulse(&pulse.Pulse{Title: "foo bar baz qux 123"})
		require.NotNil(t, err)
		require.Nil(t, pm)
	})

	t.Run("valid payload", func(t *testing.T) {
		handler := func(w http.ResponseWriter, r *http.Request) {
			respMock := []interface{}{
				"id",
				float64(1591614631576),
				nil,
				"uid",
				nil,
				"title",
				"content",
				nil,
				nil,
				1,
				1,
				nil,
				[]interface{}{"tag1", "tag2"},
				[]interface{}{"attach1", "attach2"},
				nil,
				5,
				nil,
				nil,
				nil,
			}
			payload, _ := json.Marshal(respMock)
			_, err := w.Write(payload)
			require.Nil(t, err)
		}

		server := httptest.NewServer(http.HandlerFunc(handler))
		defer server.Close()

		c := NewClientWithURL(server.URL)
		pm, err := c.Pulse.AddPulse(&pulse.Pulse{Title: "foo bar baz qux 123"})
		require.Nil(t, err)

		expected := &pulse.Pulse{
			ID:          "id",
			MTS:         1591614631576,
			UserID:      "uid",
			Title:       "title",
			Content:     "content",
			IsPin:       1,
			IsPublic:    1,
			Tags:        []string{"tag1", "tag2"},
			Attachments: []string{"attach1", "attach2"},
			Likes:       5,
		}

		assert.Equal(t, expected, pm)
	})
}

func TestPulseHistory(t *testing.T) {
	t.Run("response data slice too short", func(t *testing.T) {
		handler := func(w http.ResponseWriter, r *http.Request) {
			assert.Equal(t, "/auth/r/pulse/hist?isPublic=0", r.RequestURI)
			isPublic := r.URL.Query().Get("isPublic")
			assert.Equal(t, "0", isPublic)

			respMock := []interface{}{
				[]interface{}{"id"},
			}
			payload, _ := json.Marshal(respMock)
			_, err := w.Write(payload)
			require.Nil(t, err)
		}

		server := httptest.NewServer(http.HandlerFunc(handler))
		defer server.Close()

		c := NewClientWithURL(server.URL)
		pp, err := c.Pulse.PulseHistory(0)
		require.NotNil(t, err)
		require.Nil(t, pp)
	})

	t.Run("isPublic 0", func(t *testing.T) {
		handler := func(w http.ResponseWriter, r *http.Request) {
			isPublic := r.URL.Query().Get("isPublic")
			assert.Equal(t, "0", isPublic)

			respMock := []interface{}{
				[]interface{}{
					"id",
					float64(1591614631576),
					nil,
					"uid",
					nil,
					"title",
					"content",
					nil,
					nil,
					1,
					1,
					nil,
					[]interface{}{"tag1", "tag2"},
					[]interface{}{"attach1", "attach2"},
					nil,
					5,
					nil,
					nil,
					nil,
				},
			}
			payload, _ := json.Marshal(respMock)
			_, err := w.Write(payload)
			require.Nil(t, err)
		}

		server := httptest.NewServer(http.HandlerFunc(handler))
		defer server.Close()

		c := NewClientWithURL(server.URL)
		pm, err := c.Pulse.PulseHistory(0)
		require.Nil(t, err)

		expected := &pulse.Pulse{
			ID:          "id",
			MTS:         1591614631576,
			UserID:      "uid",
			Title:       "title",
			Content:     "content",
			IsPin:       1,
			IsPublic:    1,
			Tags:        []string{"tag1", "tag2"},
			Attachments: []string{"attach1", "attach2"},
			Likes:       5,
		}

		assert.Equal(t, expected, pm[0])
	})

	t.Run("isPublic 1", func(t *testing.T) {
		handler := func(w http.ResponseWriter, r *http.Request) {
			isPublic := r.URL.Query().Get("isPublic")
			assert.Equal(t, "1", isPublic)

			respMock := []interface{}{
				[]interface{}{
					"id",
					float64(1591614631576),
					nil,
					"uid",
					nil,
					"title",
					"content",
					nil,
					nil,
					1,
					1,
					nil,
					[]interface{}{"tag1", "tag2"},
					[]interface{}{"attach1", "attach2"},
					nil,
					5,
					nil,
					nil,
					[]interface{}{
						[]interface{}{
							"abc123",
							float64(1591614631576),
							nil,
							"nickname",
							nil,
							"picture",
							"text",
							nil,
							nil,
							"twitter",
							nil,
							30,
							5,
							nil,
						},
					},
				},
			}
			payload, _ := json.Marshal(respMock)
			_, err := w.Write(payload)
			require.Nil(t, err)
		}

		server := httptest.NewServer(http.HandlerFunc(handler))
		defer server.Close()

		c := NewClientWithURL(server.URL)
		pm, err := c.Pulse.PulseHistory(1)
		require.Nil(t, err)

		expected := &pulse.Pulse{
			ID:          "id",
			MTS:         1591614631576,
			UserID:      "uid",
			Title:       "title",
			Content:     "content",
			IsPin:       1,
			IsPublic:    1,
			Tags:        []string{"tag1", "tag2"},
			Attachments: []string{"attach1", "attach2"},
			Likes:       5,
			PulseProfile: &pulseprofile.PulseProfile{
				ID:            "abc123",
				MTS:           1591614631576,
				Nickname:      "nickname",
				Picture:       "picture",
				Text:          "text",
				TwitterHandle: "twitter",
			},
		}

		assert.Equal(t, expected, pm[0])
	})
}

func TestDeletePulse(t *testing.T) {
	t.Run("response", func(t *testing.T) {
		handler := func(w http.ResponseWriter, r *http.Request) {
			assert.Equal(t, "/auth/w/pulse/del", r.RequestURI)

			respMock := []interface{}{1}
			payload, _ := json.Marshal(respMock)
			_, err := w.Write(payload)
			require.Nil(t, err)
		}

		server := httptest.NewServer(http.HandlerFunc(handler))
		defer server.Close()

		c := NewClientWithURL(server.URL)
		deleted, err := c.Pulse.DeletePulse("abc123")
		require.Nil(t, err)
		assert.Equal(t, 1, deleted)
	})
}
<|endoftext|>


<commit_before>// Copyright 2017 The ACH Authors
// Use of this source code is governed by an Apache License
// license that can be found in the LICENSE file.

package ach

import (
	"strings"
	"testing"
)

func mockAddenda() *Addenda {
	a, _ := NewAddenda()
	addenda := a.(*Addenda)
	addenda.EntryDetailSequenceNumber = 1234567
	return addenda
}

func TestMockAddenda(t *testing.T) {
	addenda := mockAddenda()
	if err := addenda.Validate(); err != nil {
		t.Error("mockAddenda does not validate and will break other tests")
	}
	if addenda.EntryDetailSequenceNumber != 1234567 {
		t.Error("EntryDetailSequenceNumber dependent default value has changed")
	}
}

func TestParseAddenda(t *testing.T) {
	var line = "705WEB                                        DIEGO MAY                            00010000001"

	r := NewReader(strings.NewReader(line))
	r.addCurrentBatch(NewBatchPPD())
	r.currentBatch.GetHeader().StandardEntryClassCode = "PPD"
	r.currentBatch.AddEntry(&EntryDetail{TransactionCode: 22, AddendaRecordIndicator: 1})
	r.line = line
	err := r.parseAddenda()
	if err != nil {
		t.Errorf("%T: %s", err, err)
	}
	record := r.currentBatch.GetEntries()[0].Addendum[0].(*Addenda)

	if record.recordType != "7" {
		t.Errorf("RecordType Expected '7' got: %v", record.recordType)
	}
	if record.TypeCode() != "05" {
		t.Errorf("TypeCode Expected 10 got: %v", record.TypeCode())
	}
	if record.PaymentRelatedInformationField() != "WEB                                        DIEGO MAY                            " {
		t.Errorf("PaymentRelatedInformation Expected 'WEB                                        DIEGO MAY                            ' got: %v", record.PaymentRelatedInformationField())
	}
	if record.SequenceNumberField() != "0001" {
		t.Errorf("SequenceNumber Expected '0001' got: %v", record.SequenceNumberField())
	}
	if record.EntryDetailSequenceNumberField() != "0000001" {
		t.Errorf("EntryDetailSequenceNumber Expected '0000001' got: %v", record.EntryDetailSequenceNumberField())
	}
}

// TestAddendaString validats that a known parsed file can be return to a string of the same value
func TestAddendaString(t *testing.T) {
	var line = "705WEB                                        DIEGO MAY                            00010000001"
	r := NewReader(strings.NewReader(line))
	r.addCurrentBatch(NewBatchPPD())
	r.currentBatch.GetHeader().StandardEntryClassCode = "PPD"
	r.currentBatch.AddEntry(&EntryDetail{AddendaRecordIndicator: 1})
	r.line = line
	err := r.parseAddenda()
	if err != nil {
		t.Errorf("%T: %s", err, err)
	}
	record := r.currentBatch.GetEntries()[0].Addendum[0]
	if record.String() != line {
		t.Errorf("Strings do not match")
	}
}

func TestValidateAddendaRecordType(t *testing.T) {
	addenda := mockAddenda()
	addenda.recordType = "2"
	if err := addenda.Validate(); err != nil {
		if e, ok := err.(*FieldError); ok {
			if e.FieldName != "recordType" {
				t.Errorf("%T: %s", err, err)
			}
		}
	}
}

func TestValidateAddendaTypeCode(t *testing.T) {
	addenda := mockAddenda()
	addenda.typeCode = "23"
	if err := addenda.Validate(); err != nil {
		if e, ok := err.(*FieldError); ok {
			if e.FieldName != "TypeCode" {
				t.Errorf("%T: %s", err, err)
			}
		}
	}
}

func TestAddendaFieldInclusion(t *testing.T) {
	addenda := mockAddenda()
	addenda.EntryDetailSequenceNumber = 0
	if err := addenda.Validate(); err != nil {
		if e, ok := err.(*FieldError); ok {
			if e.FieldName != "EntryDetailSequenceNumber" {
				t.Errorf("%T: %s", err, err)
			}
		}
	}
}

func TestAddendaFieldInclusionRecordType(t *testing.T) {
	addenda := mockAddenda()
	addenda.recordType = ""
	if err := addenda.Validate(); err != nil {
		if e, ok := err.(*FieldError); ok {
			if e.Msg != msgFieldInclusion {
				t.Errorf("%T: %s", err, err)
			}
		}
	}
}

func TestAddendaPaymentRelatedInformationAlphaNumeric(t *testing.T) {
	addenda := mockAddenda()
	addenda.PaymentRelatedInformation = "®©"
	if err := addenda.Validate(); err != nil {
		if e, ok := err.(*FieldError); ok {
			if e.FieldName != "PaymentRelatedInformation" {
				t.Errorf("%T: %s", err, err)
			}
		}
	}
}

func TestAddendaTyeCodeNil(t *testing.T) {
	addenda := mockAddenda()
	addenda.typeCode = ""
	if err := addenda.Validate(); err != nil {
		if e, ok := err.(*FieldError); ok {
			if e.FieldName != "TypeCode" {
				t.Errorf("%T: %s", err, err)
			}
		}
	}
}
<commit_msg>Just use type *Addenda in mock rather than a type conversion<commit_after>// Copyright 2017 The ACH Authors
// Use of this source code is governed by an Apache License
// license that can be found in the LICENSE file.

package ach

import (
	"strings"
	"testing"
)

func mockAddenda() *Addenda {
	addenda := Addenda{
		recordType:                "7",
		typeCode:                  "05",
		SequenceNumber:            1,
		EntryDetailSequenceNumber: 1234567,
	}
	return &addenda
}

func TestMockAddenda(t *testing.T) {
	addenda := mockAddenda()
	if err := addenda.Validate(); err != nil {
		t.Error("mockAddenda does not validate and will break other tests")
	}
	if addenda.EntryDetailSequenceNumber != 1234567 {
		t.Error("EntryDetailSequenceNumber dependent default value has changed")
	}
}

func TestParseAddenda(t *testing.T) {
	var line = "705WEB                                        DIEGO MAY                            00010000001"

	r := NewReader(strings.NewReader(line))
	r.addCurrentBatch(NewBatchPPD())
	r.currentBatch.GetHeader().StandardEntryClassCode = "PPD"
	r.currentBatch.AddEntry(&EntryDetail{TransactionCode: 22, AddendaRecordIndicator: 1})
	r.line = line
	err := r.parseAddenda()
	if err != nil {
		t.Errorf("%T: %s", err, err)
	}
	record := r.currentBatch.GetEntries()[0].Addendum[0].(*Addenda)

	if record.recordType != "7" {
		t.Errorf("RecordType Expected '7' got: %v", record.recordType)
	}
	if record.TypeCode() != "05" {
		t.Errorf("TypeCode Expected 10 got: %v", record.TypeCode())
	}
	if record.PaymentRelatedInformationField() != "WEB                                        DIEGO MAY                            " {
		t.Errorf("PaymentRelatedInformation Expected 'WEB                                        DIEGO MAY                            ' got: %v", record.PaymentRelatedInformationField())
	}
	if record.SequenceNumberField() != "0001" {
		t.Errorf("SequenceNumber Expected '0001' got: %v", record.SequenceNumberField())
	}
	if record.EntryDetailSequenceNumberField() != "0000001" {
		t.Errorf("EntryDetailSequenceNumber Expected '0000001' got: %v", record.EntryDetailSequenceNumberField())
	}
}

// TestAddendaString validats that a known parsed file can be return to a string of the same value
func TestAddendaString(t *testing.T) {
	var line = "705WEB                                        DIEGO MAY                            00010000001"
	r := NewReader(strings.NewReader(line))
	r.addCurrentBatch(NewBatchPPD())
	r.currentBatch.GetHeader().StandardEntryClassCode = "PPD"
	r.currentBatch.AddEntry(&EntryDetail{AddendaRecordIndicator: 1})
	r.line = line
	err := r.parseAddenda()
	if err != nil {
		t.Errorf("%T: %s", err, err)
	}
	record := r.currentBatch.GetEntries()[0].Addendum[0]
	if record.String() != line {
		t.Errorf("Strings do not match")
	}
}

func TestValidateAddendaRecordType(t *testing.T) {
	addenda := mockAddenda()
	addenda.recordType = "2"
	if err := addenda.Validate(); err != nil {
		if e, ok := err.(*FieldError); ok {
			if e.FieldName != "recordType" {
				t.Errorf("%T: %s", err, err)
			}
		}
	}
}

func TestValidateAddendaTypeCode(t *testing.T) {
	addenda := mockAddenda()
	addenda.typeCode = "23"
	if err := addenda.Validate(); err != nil {
		if e, ok := err.(*FieldError); ok {
			if e.FieldName != "TypeCode" {
				t.Errorf("%T: %s", err, err)
			}
		}
	}
}

func TestAddendaFieldInclusion(t *testing.T) {
	addenda := mockAddenda()
	addenda.EntryDetailSequenceNumber = 0
	if err := addenda.Validate(); err != nil {
		if e, ok := err.(*FieldError); ok {
			if e.FieldName != "EntryDetailSequenceNumber" {
				t.Errorf("%T: %s", err, err)
			}
		}
	}
}

func TestAddendaFieldInclusionRecordType(t *testing.T) {
	addenda := mockAddenda()
	addenda.recordType = ""
	if err := addenda.Validate(); err != nil {
		if e, ok := err.(*FieldError); ok {
			if e.Msg != msgFieldInclusion {
				t.Errorf("%T: %s", err, err)
			}
		}
	}
}

func TestAddendaPaymentRelatedInformationAlphaNumeric(t *testing.T) {
	addenda := mockAddenda()
	addenda.PaymentRelatedInformation = "®©"
	if err := addenda.Validate(); err != nil {
		if e, ok := err.(*FieldError); ok {
			if e.FieldName != "PaymentRelatedInformation" {
				t.Errorf("%T: %s", err, err)
			}
		}
	}
}

func TestAddendaTyeCodeNil(t *testing.T) {
	addenda := mockAddenda()
	addenda.typeCode = ""
	if err := addenda.Validate(); err != nil {
		if e, ok := err.(*FieldError); ok {
			if e.FieldName != "TypeCode" {
				t.Errorf("%T: %s", err, err)
			}
		}
	}
}
<|endoftext|>


<commit_before>package saml2

import (
	"encoding/xml"
	"strings"
)

// Attribute holds the assertion name/values returned by the remote hosts
type Attribute struct {
	FriendlyName string    `xml:"FriendlyName,attr"`
	Name         string    `xml:"Name,attr"`
	NameFormat   string    `xml:"NameFormat,attr"`
	Values       []AttrVal `xml:"AttributeValue"`
}

// Value is an abstraction for the
type AttrVal string

// UnmarshalXML implements xml.Unmarshaler
func (v *AttrVal) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
	var s string
	err := d.DecodeElement(&s, &start)
	if err != nil {
		return err
	}
	*v = AttrVal(strings.TrimSpace(s))
	return nil
}

// Values is a type for holding Assertion Values which may be multi-valued
type Values map[string]Attribute

// Get is a safe method (nil maps will not panic) for returning the first value
// for an assertion key.
func (vals Values) Get(k string) string {
	if vals == nil {
		return ""
	}
	if v, ok := vals[k]; ok && len(v.Values) > 0 {
		return string(v.Values[0])
	}
	return ""
}

// Set replaces any pre-existing key's values (if any existed) with only the
// given value.
func (vals Values) Set(k, v string) {
	vals[k] = Attribute{Values: []AttrVal{AttrVal(v)}}
}

// Add appends to any set of values, whether or not the key existed already.
// That is, it will create a slice if none existed.
func (vals Values) Add(k, v string) {
	if _, ok := vals[k]; !ok {
		vals.Set(k, v)
		return
	}
	val := vals[k]
	val.Values = append(val.Values, AttrVal(v))
	vals[k] = val
}
<commit_msg>Update documentation, add Del method<commit_after>package saml2

import (
	"encoding/xml"
	"strings"
)

// Attribute holds the assertion name/values returned by the remote hosts
type Attribute struct {
	FriendlyName string    `xml:"FriendlyName,attr"`
	Name         string    `xml:"Name,attr"`
	NameFormat   string    `xml:"NameFormat,attr"`
	Values       []AttrVal `xml:"AttributeValue"`
}

// AttrVal is an abstraction for the string value of an XML document, which will
// ensure that all surrounding space is trimmed during Unmarshaling
type AttrVal string

// UnmarshalXML implements xml.Unmarshaler
func (v *AttrVal) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
	var s string
	err := d.DecodeElement(&s, &start)
	if err != nil {
		return err
	}
	*v = AttrVal(strings.TrimSpace(s))
	return nil
}

// Values is a convenience wrapper for a map of strings to Attributes, which
// can be used for easy access to the string values of Attribute lists.
type Values map[string]Attribute

// Get is a safe method (nil maps will not panic) for returning the first value
// for an attribute at a key, or the empty string if none exists.
func (vals Values) Get(k string) string {
	if vals == nil {
		return ""
	}
	if v, ok := vals[k]; ok && len(v.Values) > 0 {
		return string(v.Values[0])
	}
	return ""
}

// Set replaces any pre-existing key's values (if any existed) with an
// attribute containing only the given value.
func (vals Values) Set(k, v string) {
	vals[k] = Attribute{Values: []AttrVal{AttrVal(v)}}
}

// Add appends to any Attribute's set of values, whether or not the key existed
// already. That is, it will create an attribute with a one-length slice if none
// existed.
func (vals Values) Add(k, v string) {
	if _, ok := vals[k]; !ok {
		vals.Set(k, v)
		return
	}
	val := vals[k]
	val.Values = append(val.Values, AttrVal(v))
	vals[k] = val
}

// Delete implements a quick key delete
func (vals Values) Del(k) {
	delete(vals, k)
}
<|endoftext|>


<commit_before>package main

import (
	"net/http"
	"os"
	"fmt"
	"log"
	"github.com/nu7hatch/gouuid"
	"github.com/vaughan0/go-ini"
)

var TriggerNotFoundMsg = "Trigger could not be found."

// A handler for a specific trigger.
type TriggerTrigger struct {
	// the path for which this trigger is triggered.
	path string
	// the URL that is to be called when this trigger i triggered
	outputurl string
}

// create a new trigger trigger for a specific path
func LoadHandler(outputurl, path string) TriggerTrigger {
	return TriggerTrigger{
		path: path,
		outputurl: outputurl,
	}
}

// log and trigger the specific trigger
//
// Does proper checks to make sure that the right method is from
// downstream.
func (tt TriggerTrigger) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	if r.Method != "GET"{
		// only supporting GET at the moment
		log.Println("[server]", r.Method, r.URL.Path, "404 (only GET allowed)")
		w.WriteHeader(http.StatusNotFound)
		fmt.Fprintf(w, TriggerNotFoundMsg)
		return
	}
	if r.URL.Path != tt.path {
		// if this isn't here we'll trigger for all path
		// prefixes.
		w.WriteHeader(http.StatusNotFound)
		fmt.Fprintf(w, TriggerNotFoundMsg)
		return
	}

	loguuid, err := uuid.NewV4()
	// used to correlate server and client log lines
	var id string
	if err != nil {
		id = "???"
	} else {
		id = loguuid.String()
	}

	log.Println("[server]", id, "GET", r.URL.Path, "200")
	fmt.Fprintf(w, "Triggered.")
	go func() {
		resp, err := http.Get(tt.outputurl)
		var result string
		if err != nil {
			result = "Err: " + err.Error()
		} else {
			result = resp.Status
		}
		log.Println("[client]", id, "GET", tt.outputurl, result)
	}()
}

// Load all handlers from the configuration file.
func LoadHandlers(file *ini.File) {
	root_found := false

	for path, _ := range *file {
		if path == "" {
			// ignoring default section
			continue
		}

		if path == "/" {
			root_found = true
		}

		outputurl, ok := file.Get(path, "url")
		if !ok {
			panic(fmt.Sprintf("'url' missing for: %s", path))
		}

		http.Handle(path, LoadHandler(outputurl, path))
	}

	if !root_found {
		// custom 404
		http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
			log.Println("[server]", r.Method, r.URL.Path, "404")
			w.WriteHeader(http.StatusNotFound)
			fmt.Fprintf(w, TriggerNotFoundMsg)
		})
	}
}

const CONFIG_FILE_ENVIRON = "TRIGGER_TRIGGER_CONFIG"

func main() {
	inifilename := os.Getenv(CONFIG_FILE_ENVIRON)
	if inifilename == "" {
		fmt.Println("Please point", CONFIG_FILE_ENVIRON, "to config file.")
		os.Exit(1)
	}

	file, err := ini.LoadFile(inifilename)
	if err != nil {
		panic(fmt.Sprintf("Could not read config file: %s", inifilename))
	}

	LoadHandlers(&file)

	listen, ok := file.Get("", "listen")
	if !ok {
		log.Print("[server] 'listen' not defined. Using fallback ':8080'")
		listen = ":8080"
	}

	log.Fatal(http.ListenAndServe(listen, nil))
}
<commit_msg>Removing pointer reference<commit_after>package main

import (
	"net/http"
	"os"
	"fmt"
	"log"
	"github.com/nu7hatch/gouuid"
	"github.com/vaughan0/go-ini"
)

var TriggerNotFoundMsg = "Trigger could not be found."

// A handler for a specific trigger.
type TriggerTrigger struct {
	// the path for which this trigger is triggered.
	path string
	// the URL that is to be called when this trigger i triggered
	outputurl string
}

// create a new trigger trigger for a specific path
func LoadHandler(outputurl, path string) TriggerTrigger {
	return TriggerTrigger{
		path: path,
		outputurl: outputurl,
	}
}

// log and trigger the specific trigger
//
// Does proper checks to make sure that the right method is from
// downstream.
func (tt TriggerTrigger) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	if r.Method != "GET"{
		// only supporting GET at the moment
		log.Println("[server]", r.Method, r.URL.Path, "404 (only GET allowed)")
		w.WriteHeader(http.StatusNotFound)
		fmt.Fprintf(w, TriggerNotFoundMsg)
		return
	}
	if r.URL.Path != tt.path {
		// if this isn't here we'll trigger for all path
		// prefixes.
		w.WriteHeader(http.StatusNotFound)
		fmt.Fprintf(w, TriggerNotFoundMsg)
		return
	}

	loguuid, err := uuid.NewV4()
	// used to correlate server and client log lines
	var id string
	if err != nil {
		id = "???"
	} else {
		id = loguuid.String()
	}

	log.Println("[server]", id, "GET", r.URL.Path, "200")
	fmt.Fprintf(w, "Triggered.")
	go func() {
		resp, err := http.Get(tt.outputurl)
		var result string
		if err != nil {
			result = "Err: " + err.Error()
		} else {
			result = resp.Status
		}
		log.Println("[client]", id, "GET", tt.outputurl, result)
	}()
}

// Load all handlers from the configuration file.
func LoadHandlers(file ini.File) {
	root_found := false

	for path, _ := range file {
		if path == "" {
			// ignoring default section
			continue
		}

		if path == "/" {
			root_found = true
		}

		outputurl, ok := file.Get(path, "url")
		if !ok {
			panic(fmt.Sprintf("'url' missing for: %s", path))
		}

		http.Handle(path, LoadHandler(outputurl, path))
	}

	if !root_found {
		// custom 404
		http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
			log.Println("[server]", r.Method, r.URL.Path, "404")
			w.WriteHeader(http.StatusNotFound)
			fmt.Fprintf(w, TriggerNotFoundMsg)
		})
	}
}

const CONFIG_FILE_ENVIRON = "TRIGGER_TRIGGER_CONFIG"

func main() {
	inifilename := os.Getenv(CONFIG_FILE_ENVIRON)
	if inifilename == "" {
		fmt.Println("Please point", CONFIG_FILE_ENVIRON, "to config file.")
		os.Exit(1)
	}

	file, err := ini.LoadFile(inifilename)
	if err != nil {
		panic(fmt.Sprintf("Could not read config file: %s", inifilename))
	}

	LoadHandlers(file)

	listen, ok := file.Get("", "listen")
	if !ok {
		log.Print("[server] 'listen' not defined. Using fallback ':8080'")
		listen = ":8080"
	}

	log.Fatal(http.ListenAndServe(listen, nil))
}
<|endoftext|>


<commit_before>package auth

import (
  "fmt"
  "regexp"
  "net/http"
  "github.com/ricallinson/forgery"
  "github.com/spacedock-io/registry/config"
  "github.com/spacedock-io/registry/session"
)

var (
  tokenRegex = regexp.MustCompile(`^Token signature=([\w\-]+),repository=(.*?),access=(\w+)$`)
)

type Token struct {
  Signature, Repo, Access string
}

func LoadCheckToken(req *f.Request) bool {
  header := req.Get("Authorization")
  if header != ""{
    return false
  }

  extracted := tokenRegex.FindStringSubmatch(header)
  token := &Token{
    Signature: extracted[1],
    Repo: extracted[2],
    Access: extracted[3],
  }

  /*
   * Token Access must be compliant with the HTTP Method
   */
  if token.Access == "read" && req.Method != "GET" { return false }
  if token.Access == "write" && req.Method != "POST" && req.Method != "PUT" { return false }
  if token.Access == "delete" && req.Method != "DELETE" { return false }

  if token.Validate() {
    sx.Session(req).Set("token", token)
    sx.Session(req).Save()
    return true
  }
  return false
}

func (t *Token) Header() string {
  return "Token signature=" + t.Signature + ",repository=" + t.Repo + ",access="+ t.Access
}

func (t *Token) Validate() bool {
  client := &http.Client{}

  req, _ := http.NewRequest("GET",
    fmt.Sprintf("%s/v1/repositories/%s/auth",
      config.Global.Get("index").Str(), t.Repo), nil)
  req.Header.Add("Authorization", t.Header())

  resp, err := client.Do(req)
  if err == nil && resp.StatusCode == 200 {
    return true
  }
  return false
}

func Secure(route func(*f.Request, *f.Response)) func(*f.Request, *f.Response, func()) {
  /*
   * Two types of auth are valid: Token or Session
   */
  return func(req *f.Request, res *f.Response, next func()) {
    defer next()
    if sx.Session(req).Get("token") != nil || LoadCheckToken(req) {
      route(req, res)
    } else {
      res.Send(401)
    }
  }
}
<commit_msg>Temporarily remove auth checks until they can be improved<commit_after>package auth

import (
  "fmt"
  "regexp"
  "net/http"
  "github.com/ricallinson/forgery"
  "github.com/spacedock-io/registry/config"
  "github.com/spacedock-io/registry/session"
)

var (
  tokenRegex = regexp.MustCompile(`^Token signature=([\w\-]+),repository=(.*?),access=(\w+)$`)
)

type Token struct {
  Signature, Repo, Access string
}

func LoadCheckToken(req *f.Request) bool {
  header := req.Get("Authorization")
  if header != ""{
    return false
  }

  extracted := tokenRegex.FindStringSubmatch(header)
  token := &Token{
    Signature: extracted[1],
    Repo: extracted[2],
    Access: extracted[3],
  }

  /*
   * Token Access must be compliant with the HTTP Method
  if token.Access == "read" && req.Method != "GET" { return false }
  if token.Access == "write" && req.Method != "POST" && req.Method != "PUT" { return false }
  if token.Access == "delete" && req.Method != "DELETE" { return false }
  */

  if token.Validate() {
    sx.Session(req).Set("token", token)
    sx.Session(req).Save()
    return true
  }
  return false
}

func (t *Token) Header() string {
  return "Token signature=" + t.Signature + ",repository=" + t.Repo + ",access="+ t.Access
}

func (t *Token) Validate() bool {
  client := &http.Client{}

  req, _ := http.NewRequest("GET",
    fmt.Sprintf("%s/v1/repositories/%s/auth",
      config.Global.Get("index").Str(), t.Repo), nil)
  req.Header.Add("Authorization", t.Header())

  resp, err := client.Do(req)
  if err == nil && resp.StatusCode == 200 {
    return true
  }
  return false
}

func Secure(route func(*f.Request, *f.Response)) func(*f.Request, *f.Response, func()) {
  /*
   * Two types of auth are valid: Token or Session
   */
  return func(req *f.Request, res *f.Response, next func()) {
    defer next()
    if sx.Session(req).Get("token") != nil || LoadCheckToken(req) {
      route(req, res)
    } else {
      res.Send(401)
    }
  }
}
<|endoftext|>


<commit_before>package server

import (
	"testing"

	"github.com/go-kit/kit/endpoint"
	"github.com/kolide/kolide-ose/datastore"
	"github.com/stretchr/testify/assert"
	"golang.org/x/net/context"
)

// TestEndpointPermissions tests that
// the endpoint.Middleware correctly grants or denies
// permissions to access or modify resources
func TestEndpointPermissions(t *testing.T) {
	ctx := context.Background()
	req := struct{}{}
	ds, _ := datastore.New("inmem", "")
	createTestUsers(t, ds)
	admin1, _ := ds.User("admin1")
	user1, _ := ds.User("user1")
	user2, _ := ds.User("user2")
	user2.Enabled = false

	e := endpoint.Nop // a test endpoint
	var endpointTests = []struct {
		endpoint endpoint.Endpoint
		// who is making the request
		vc *viewerContext
		// what resource are we editing
		requestID uint
		// what error to expect
		wantErr interface{}
	}{
		{
			endpoint: mustBeAdmin(e),
			wantErr:  errNoContext,
		},
		{
			endpoint: canReadUser(e),
			wantErr:  errNoContext,
		},
		{
			endpoint: canModifyUser(e),
			wantErr:  errNoContext,
		},
		{
			endpoint: mustBeAdmin(e),
			vc:       &viewerContext{user: admin1},
		},
		{
			endpoint: mustBeAdmin(e),
			vc:       &viewerContext{user: user1},
			wantErr:  forbiddenError{"must be an admin"},
		},
		{
			endpoint: canModifyUser(e),
			vc:       &viewerContext{user: admin1},
		},
		{
			endpoint: canModifyUser(e),
			vc:       &viewerContext{user: user1},
			wantErr:  forbiddenError{message: "no write permissions on user"},
		},
		{
			endpoint:  canModifyUser(e),
			vc:        &viewerContext{user: user1},
			requestID: admin1.ID,
			wantErr:   forbiddenError{message: "no write permissions on user"},
		},
		{
			endpoint:  canReadUser(e),
			vc:        &viewerContext{user: user1},
			requestID: admin1.ID,
		},
		{
			endpoint:  canReadUser(e),
			vc:        &viewerContext{user: user2},
			requestID: admin1.ID,
			wantErr:   forbiddenError{message: "no read permissions on user"},
		},
	}

	for _, tt := range endpointTests {
		if tt.vc != nil {
			ctx = context.WithValue(ctx, "viewerContext", tt.vc)
		}
		if tt.requestID != 0 {
			ctx = context.WithValue(ctx, "request-id", tt.requestID)
		}
		_, eerr := tt.endpoint(ctx, req)
		assert.IsType(t, tt.wantErr, eerr)
		if ferr, ok := eerr.(forbiddenError); ok {
			assert.Equal(t, tt.wantErr.(forbiddenError).message, ferr.message)
		}
	}
}
<commit_msg>Simplify test error check (#167)<commit_after>package server

import (
	"testing"

	"github.com/go-kit/kit/endpoint"
	"github.com/kolide/kolide-ose/datastore"
	"github.com/stretchr/testify/assert"
	"golang.org/x/net/context"
)

// TestEndpointPermissions tests that
// the endpoint.Middleware correctly grants or denies
// permissions to access or modify resources
func TestEndpointPermissions(t *testing.T) {
	ctx := context.Background()
	req := struct{}{}
	ds, _ := datastore.New("inmem", "")
	createTestUsers(t, ds)
	admin1, _ := ds.User("admin1")
	user1, _ := ds.User("user1")
	user2, _ := ds.User("user2")
	user2.Enabled = false

	e := endpoint.Nop // a test endpoint
	var endpointTests = []struct {
		endpoint endpoint.Endpoint
		// who is making the request
		vc *viewerContext
		// what resource are we editing
		requestID uint
		// what error to expect
		wantErr interface{}
	}{
		{
			endpoint: mustBeAdmin(e),
			wantErr:  errNoContext,
		},
		{
			endpoint: canReadUser(e),
			wantErr:  errNoContext,
		},
		{
			endpoint: canModifyUser(e),
			wantErr:  errNoContext,
		},
		{
			endpoint: mustBeAdmin(e),
			vc:       &viewerContext{user: admin1},
		},
		{
			endpoint: mustBeAdmin(e),
			vc:       &viewerContext{user: user1},
			wantErr:  forbiddenError{"must be an admin"},
		},
		{
			endpoint: canModifyUser(e),
			vc:       &viewerContext{user: admin1},
		},
		{
			endpoint: canModifyUser(e),
			vc:       &viewerContext{user: user1},
			wantErr:  forbiddenError{message: "no write permissions on user"},
		},
		{
			endpoint:  canModifyUser(e),
			vc:        &viewerContext{user: user1},
			requestID: admin1.ID,
			wantErr:   forbiddenError{message: "no write permissions on user"},
		},
		{
			endpoint:  canReadUser(e),
			vc:        &viewerContext{user: user1},
			requestID: admin1.ID,
		},
		{
			endpoint:  canReadUser(e),
			vc:        &viewerContext{user: user2},
			requestID: admin1.ID,
			wantErr:   forbiddenError{message: "no read permissions on user"},
		},
	}

	for _, tt := range endpointTests {
		if tt.vc != nil {
			ctx = context.WithValue(ctx, "viewerContext", tt.vc)
		}
		if tt.requestID != 0 {
			ctx = context.WithValue(ctx, "request-id", tt.requestID)
		}
		_, eerr := tt.endpoint(ctx, req)
		assert.Equal(t, tt.wantErr, eerr)
	}
}
<|endoftext|>