Files
ipe/api/handlers.go
T
2018-11-26 22:55:00 +01:00

455 lines
12 KiB
Go

// Copyright 2014 Claudemiro Alves Feitosa Neto. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package api
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"sort"
"strings"
log "github.com/golang/glog"
"github.com/gorilla/mux"
"ipe/events"
"ipe/storage"
"ipe/utils"
)
// // Maximum event size permitted 10 kB
// See: http://blogs.gnome.org/cneumair/2008/09/30/1-kb-1024-bytes-no-1-kb-1000-bytes/
const maxDataEventSize = 10 * 1000
// Prepare QueryString
func prepareQueryString(params url.Values) string {
var keys []string
for key := range params {
keys = append(keys, strings.ToLower(key))
}
sort.Strings(keys)
var pieces []string
for _, key := range keys {
pieces = append(pieces, key+"="+params.Get(key))
}
return strings.Join(pieces, "&")
}
// Authentication Authenticate pusher
// see: https://gist.github.com/mloughran/376898
//
// The signature is a HMAC SHA256 hex digest.
// This is generated by signing a string made up of the following components concatenated with newline characters \n.
//
// * The uppercase request method (e.g. POST)
// * The request path (e.g. /some/resource)
// * The query parameters sorted by key, with keys converted to lowercase, then joined as in the query string.
// Note that the string must not be url escaped (e.g. given the keys auth_key: foo, Name: Something else, you get auth_key=foo&name=Something else)
func Authentication(storage storage.Storage) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
var (
pathVars = mux.Vars(r)
appID = pathVars["app_id"]
)
app, err := storage.GetAppByAppID(appID)
if err != nil {
log.Error(err)
http.Error(w, "Not authorized", http.StatusUnauthorized)
return
}
query := r.URL.Query()
signature := query.Get("auth_signature")
query.Del("auth_signature")
queryString := prepareQueryString(query)
toSign := strings.ToUpper(r.Method) + "\n" + r.URL.Path + "\n" + queryString
if utils.HashMAC([]byte(toSign), []byte(app.Secret)) == signature {
next.ServeHTTP(w, r)
} else {
log.Error("Not authorized")
http.Error(w, "Not authorized", http.StatusUnauthorized)
}
}
return http.HandlerFunc(fn)
}
}
// CheckAppDisabled Check if the application is disabled
func CheckAppDisabled(storage storage.Storage) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
var (
pathVars = mux.Vars(r)
appID = pathVars["app_id"]
)
currentApp, err := storage.GetAppByAppID(appID)
if err != nil {
http.Error(w, fmt.Sprintf("Could not found an app with app_id: %s", appID), http.StatusForbidden)
return
}
if !currentApp.Enabled {
http.Error(w, "Application disabled", http.StatusForbidden)
return
}
next.ServeHTTP(w, r)
}
return http.HandlerFunc(fn)
}
}
// PostEvents handle post events
type PostEvents struct{ storage storage.Storage }
// NewPostEvents return a new PostEvents handler
func NewPostEvents(storage storage.Storage) *PostEvents {
return &PostEvents{storage: storage}
}
// ServeHTTP An event consists of a name and data (typically JSON) which may be sent to all subscribers to a particular channel or channels.
// This is conventionally known as triggering an event.
//
// The body should contain a Hash of parameters encoded as JSON where data parameter itself is JSON encoded.
//
// Not Implemented:
// Note that these parameters may be provided in the query string, although this is discouraged.
//
// Example:
//
// {"name":"foo","channels":["project-3"],"data":"{\"some\":\"data\"}"}
//
// Response is an empty JSON hash.
//
// POST /apps/{app_id}/events
func (h *PostEvents) ServeHTTP(w http.ResponseWriter, r *http.Request) {
var (
pathVars = mux.Vars(r)
appID = pathVars["app_id"]
)
app, err := h.storage.GetAppByAppID(appID)
if err != nil {
http.Error(w, fmt.Sprintf("Could not found an app with app_id: %s", appID), http.StatusBadRequest)
}
var input struct {
Name string `json:"name"`
Data json.RawMessage `json:"data"`
Channels []string `json:"channels,omitempty"`
Channel string `json:"channel,omitempty"`
SocketID string `json:"socket_id,omitempty"`
}
err = json.NewDecoder(r.Body).Decode(&input)
if err != nil {
http.Error(w, "Bad request", http.StatusBadRequest)
return
}
// The event data should not be larger than 10KB.
if len(input.Data) > maxDataEventSize {
http.Error(w, "Request too large.", http.StatusRequestEntityTooLarge)
return
}
log.Info(input.Channels)
if len(input.Channel) > 0 && len(input.Channels) == 0 {
input.Channels = append(input.Channels, input.Channel)
}
for _, c := range input.Channels {
channel := app.FindOrCreateChannelByChannelID(c)
if err := app.Publish(channel, events.Raw{Event: input.Name, Channel: c, Data: input.Data}, input.SocketID); err != nil {
log.Errorf("error publishing event %+v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
if _, err := w.Write([]byte("{}")); err != nil {
log.Errorf("unexpected error while writing into response %+v", err)
}
}
// GetChannels handle get channels
type GetChannels struct{ storage storage.Storage }
// NewGetChannels return a new GetChannels handler
func NewGetChannels(storage storage.Storage) *GetChannels {
return &GetChannels{storage: storage}
}
// ServeHTTP Allows fetching a hash of occupied channels (optionally filtered by prefix),
// and optionally one or more attributes for each channel.
//
// Notes:
// 'user_count' is the only attribute documented on the Pusher API
//
// Example:
// {
// "channels": {
// "presence-foobar": {
// user_count: 42
// },
// "presence-another": {
// user_count: 123
// }
// }
// }
//
// GET /apps/{app_id}/channels
func (h *GetChannels) ServeHTTP(w http.ResponseWriter, r *http.Request) {
var (
pathVars = mux.Vars(r)
queryVars = r.URL.Query()
appID = pathVars["app_id"]
filter = queryVars.Get("filter_by_prefix")
info = queryVars.Get("info")
attributes = strings.Split(info, ",")
)
requestedUserCount := false
for _, a := range attributes {
if a == "user_count" {
requestedUserCount = true
}
}
// If an attribute such as user_count is requested, and the request is not limited
// to presence channels, the API will return an error (400 code)
if requestedUserCount && filter != "presence-" {
http.Error(w, "Attribute user_count is restricted to presence channels", http.StatusBadRequest)
return
}
app, err := h.storage.GetAppByAppID(appID)
if err != nil {
http.Error(w, fmt.Sprintf("Could not found an app with app_id: %s", appID), http.StatusBadRequest)
}
channels := make(map[string]interface{})
switch filter {
case "presence-":
for _, c := range app.PresenceChannels() {
if requestedUserCount {
channels[c.ID] = struct {
UserCount int `json:"user_count"`
}{
c.TotalUsers(),
}
} else {
channels[c.ID] = struct{}{}
}
}
case "public-":
for _, c := range app.PublicChannels() {
channels[c.ID] = struct{}{}
}
case "private-":
for _, c := range app.PrivateChannels() {
channels[c.ID] = struct{}{}
}
default:
for _, c := range app.Channels() {
channels[c.ID] = struct{}{}
}
}
w.Header().Set("Content-Type", "application/json")
js := make(map[string]interface{}, 1)
js["channels"] = channels
if err := json.NewEncoder(w).Encode(js); err != nil {
log.Error(err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
}
}
// GetChannel handle get channel
type GetChannel struct{ storage storage.Storage }
// NewGetChannel return a new GetChannel handler
func NewGetChannel(storage storage.Storage) *GetChannel {
return &GetChannel{storage: storage}
}
// ServeHTTP Fetch info for one channel
//
// Example:
// {
// occupied: true,
// user_count: 42,
// subscription_count: 42
// }
//
// GET /apps/{app_id}/channels/{channel_name}
func (h *GetChannel) ServeHTTP(w http.ResponseWriter, r *http.Request) {
var (
pathVars = mux.Vars(r)
queryVars = r.URL.Query()
appID = pathVars["app_id"]
channelName = pathVars["channel_name"]
info = queryVars.Get("info")
attributes = strings.Split(info, ",")
)
app, err := h.storage.GetAppByAppID(appID)
if err != nil {
http.Error(w, fmt.Sprintf("Could not found an app with app_id: %s", appID), http.StatusBadRequest)
}
// Channel name could not be empty
if strings.TrimSpace(channelName) == "" {
http.Error(w, "Empty channel name", http.StatusBadRequest)
return
}
// Attributes requested
requestedUserCount := false
requestedSubscriptionCount := false
for _, a := range attributes {
switch a {
case "subscription_count":
requestedSubscriptionCount = true
case "user_count":
requestedUserCount = true
}
}
channel, err := app.FindChannelByChannelID(channelName)
// Channel exists?
if err != nil {
http.Error(w, fmt.Sprintf("Could not find a channel with id %s", channelName), http.StatusBadRequest)
return
}
// If an attribute such as user_count is requested, and the request is not limited
// to presence channels, the API will return an error (400 code)
if requestedUserCount && !channel.IsPresence() {
http.Error(w, "Attribute user_count is restricted to presence channels", http.StatusBadRequest)
return
}
// Output
dtoChannel := struct {
Occupied bool `json:"occupied"`
UserCount int `json:"user_count,omitempty"`
SubscriptionCount int `json:"subscription_count,omitempty"`
}{Occupied: channel.IsOccupied()}
switch {
case requestedSubscriptionCount && requestedUserCount:
dtoChannel.UserCount = channel.TotalUsers()
dtoChannel.SubscriptionCount = channel.TotalSubscriptions()
case requestedUserCount:
dtoChannel.UserCount = channel.TotalUsers()
case requestedSubscriptionCount:
dtoChannel.SubscriptionCount = channel.TotalSubscriptions()
}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(dtoChannel); err != nil {
log.Error(err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
}
}
// GetChannelUsers handle get users from a channel
type GetChannelUsers struct{ storage storage.Storage }
// NewGetChannelUsers return a new GetChannelUsers handler
func NewGetChannelUsers(storage storage.Storage) *GetChannelUsers {
return &GetChannelUsers{storage: storage}
}
// ServeHTTP Allowed only for presence-channels
//
// Example:
// {
// "users": [
// { "id": "1" },
// { "id": "2" }
// ]
// }
//
// GET /apps/{app_id}/channels/{channel_name}/users
func (h *GetChannelUsers) ServeHTTP(w http.ResponseWriter, r *http.Request) {
var (
pathVars = mux.Vars(r)
appID = pathVars["app_id"]
channelName = pathVars["channel_name"]
)
isPresence := utils.IsPresenceChannel(channelName)
if !isPresence {
http.Error(w, "This api endpoint is restricted to presence channels.", http.StatusBadRequest)
return
}
app, err := h.storage.GetAppByAppID(appID)
if err != nil {
http.Error(w, fmt.Sprintf("Could not found an app with app_id: %s", appID), http.StatusBadRequest)
}
// Get the channel
channel, err := app.FindChannelByChannelID(channelName)
// Channel exists?
if err != nil {
http.Error(w, fmt.Sprintf("Could not find a channel with id %s", channelName), http.StatusBadRequest)
return
}
result := make(map[string][]interface{})
var users []interface{}
for _, s := range channel.Subscriptions() {
users = append(users, struct {
ID string `json:"id"`
}{s.ID})
}
result["users"] = users
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(result); err != nil {
http.Error(w, "Internal server error", http.StatusInternalServerError)
log.Error(err)
}
}