Add initial http server code

This commit is contained in:
Tobie Morgan Hitchcock 2016-02-26 17:26:03 +00:00
parent 79b756dde0
commit 7501d7c78b
11 changed files with 608 additions and 0 deletions

27
server/api/api.go Normal file
View file

@ -0,0 +1,27 @@
// Copyright © 2016 Abcum Ltd
//
// 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 api
import (
"github.com/labstack/echo"
)
func code(code int) *echo.HTTPError {
return echo.NewHTTPError(code, "")
}
func sock(c *echo.Context) bool {
return c.Request().Header.Get(echo.Upgrade) == echo.WebSocket
}

32
server/api/auth.go Normal file
View file

@ -0,0 +1,32 @@
// Copyright © 2016 Abcum Ltd
//
// 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 api
import (
"github.com/labstack/echo"
)
// AuthOpts defines options for the Head middleward
type AuthOpts struct {
}
// Auth defines middleware for reading JWT authentication tokens
func Auth(opts *AuthOpts) echo.MiddlewareFunc {
return func(h echo.HandlerFunc) echo.HandlerFunc {
return func(c *echo.Context) error {
return h(c)
}
}
}

84
server/api/cors.go Normal file
View file

@ -0,0 +1,84 @@
// Copyright © 2016 Abcum Ltd
//
// 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 api
import (
"net/http"
"strconv"
"strings"
"github.com/labstack/echo"
)
// CorsOpts defines options for the Head middleward
type CorsOpts struct {
AllowedMethods []string
AllowedHeaders []string
AccessControlMaxAge int
}
// Cors defines middleware for specifying CORS headers
func Cors(opts *CorsOpts) echo.MiddlewareFunc {
return func(h echo.HandlerFunc) echo.HandlerFunc {
return func(c *echo.Context) error {
origin := c.Request().Header.Get("Origin")
if origin == "" {
return h(c)
}
// Set default values for opts.AllowedMethods
allowedMethods := opts.AllowedMethods
if len(allowedMethods) == 0 {
allowedMethods = []string{"GET", "PUT", "POST", "PATCH", "TRACE", "DELETE"}
}
// Set default values for opts.AllowedHeaders
allowedHeaders := opts.AllowedHeaders
if len(allowedHeaders) == 0 {
allowedHeaders = []string{"Accept", "Content-Type", "Origin"}
}
// Set default values for opts.AccessControlMaxAge
accessControlMaxAge := opts.AccessControlMaxAge
if accessControlMaxAge == 0 {
accessControlMaxAge = 3600
}
// Normalize AllowedMethods and make comma-separated-values
normedMethods := []string{}
for _, allowedMethod := range allowedMethods {
normed := http.CanonicalHeaderKey(allowedMethod)
normedMethods = append(normedMethods, normed)
}
// Normalize AllowedHeaders and make comma-separated-values
normedHeaders := []string{}
for _, allowedHeader := range allowedHeaders {
normed := http.CanonicalHeaderKey(allowedHeader)
normedHeaders = append(normedHeaders, normed)
}
c.Response().Header().Set("Access-Control-Allow-Methods", strings.Join(normedMethods, ","))
c.Response().Header().Set("Access-Control-Allow-Headers", strings.Join(normedHeaders, ","))
c.Response().Header().Set("Access-Control-Max-Age", strconv.Itoa(accessControlMaxAge))
c.Response().Header().Set("Access-Control-Allow-Origin", origin)
return h(c)
}
}
}

43
server/api/head.go Normal file
View file

@ -0,0 +1,43 @@
// Copyright © 2016 Abcum Ltd
//
// 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 api
import (
"github.com/labstack/echo"
)
// HeadOpts defines options for the Head middleward
type HeadOpts struct {
CacheControl string
}
// Head defines middleware for setting miscellaneous headers
func Head(opts *HeadOpts) echo.MiddlewareFunc {
return func(h echo.HandlerFunc) echo.HandlerFunc {
return func(c *echo.Context) error {
// Set default values for opts.PoweredBy
cacheControl := opts.CacheControl
if cacheControl == "" {
cacheControl = "no-cache"
}
c.Response().Header().Set("Cache-control", cacheControl)
return h(c)
}
}
}

43
server/api/info.go Normal file
View file

@ -0,0 +1,43 @@
// Copyright © 2016 Abcum Ltd
//
// 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 api
import (
"github.com/labstack/echo"
)
// InfoOpts defines options for the Head middleward
type InfoOpts struct {
PoweredBy string
}
// Info defines middleware for specifying the server powered-by header
func Info(opts *InfoOpts) echo.MiddlewareFunc {
return func(h echo.HandlerFunc) echo.HandlerFunc {
return func(c *echo.Context) error {
// Set default values for opts.PoweredBy
poweredBy := opts.PoweredBy
if poweredBy == "" {
poweredBy = "Surreal"
}
c.Response().Header().Set("X-Powered-By", poweredBy)
return h(c)
}
}
}

62
server/api/type.go Normal file
View file

@ -0,0 +1,62 @@
// Copyright © 2016 Abcum Ltd
//
// 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 api
import (
"mime"
"sort"
"github.com/labstack/echo"
)
// TypeOpts defines options for the Head middleward
type TypeOpts struct {
AllowedContent []string
}
// Type defines middleware for checking the request content-type
func Type(opts *TypeOpts) echo.MiddlewareFunc {
return func(h echo.HandlerFunc) echo.HandlerFunc {
return func(c *echo.Context) error {
// Set default values for opts.AllowedContent
allowedContent := opts.AllowedContent
if len(allowedContent) == 0 {
allowedContent = []string{echo.ApplicationJSON}
}
// Extract the Content-Type header
header := c.Request().Header.Get(echo.ContentType)
cotype, _, _ := mime.ParseMediaType(header)
// Sort and search opts.AllowedContent types
sort.Strings(allowedContent)
i := sort.SearchStrings(allowedContent, cotype)
if c.Request().ContentLength == 0 {
return h(c)
}
if c.Request().ContentLength >= 1 {
if i < len(allowedContent) {
return h(c)
}
}
return code(415)
}
}
}

36
server/crud.go Normal file
View file

@ -0,0 +1,36 @@
// Copyright © 2016 Abcum Ltd
//
// 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 server
import (
"github.com/abcum/surreal/sql"
"github.com/labstack/echo"
)
func crud(c *echo.Context) error {
s, e := sql.NewParser(c.Request().Body).Parse()
if e == nil {
return c.JSON(200, show(s))
}
if e != nil {
return c.JSON(422, oops(e))
}
return nil
}

104
server/errors.go Normal file
View file

@ -0,0 +1,104 @@
// Copyright © 2016 Abcum Ltd
//
// 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 server
import (
"github.com/labstack/echo"
)
func show(i interface{}) interface{} {
return i
}
func oops(e error) interface{} {
return map[string]interface{}{
"code": 422,
"details": "Request problems detected",
"documentation": docs,
"information": e.Error(),
}
}
func errors(err error, c *echo.Context) {
code := 500
if e, ok := err.(*echo.HTTPError); ok {
code = e.Code()
}
c.JSON(code, errs[code])
}
const docs = "https://docs.surreal.io/"
var errs = map[int]interface{}{
200: map[string]interface{}{
"code": 200,
"details": "Information",
"documentation": docs,
"information": "Visit the documentation for details on accessing the api.",
},
401: map[string]interface{}{
"code": 401,
"details": "Authentication failed",
"documentation": docs,
"information": "Your authentication details are invalid. Reauthenticate using a valid token.",
},
403: map[string]interface{}{
"code": 403,
"details": "Request resource forbidden",
"documentation": docs,
"information": "Your request was forbidden. Perhaps you don't have the necessary permissions to access this resource.",
},
404: map[string]interface{}{
"code": 404,
"details": "Request resource not found",
"documentation": docs,
"information": "The requested resource does not exist. Check that you have entered the url correctly.",
},
405: map[string]interface{}{
"code": 405,
"details": "This method is not allowed",
"documentation": docs,
"information": "The requested http method is not allowed for this resource. Refer to the documentation for allowed methods.",
},
415: map[string]interface{}{
"code": 415,
"details": "Unsupported content type requested",
"documentation": docs,
"information": "Requests to the api must use the 'Content-Type: application/json' header. Check your request settings and try again.",
},
422: map[string]interface{}{
"code": 422,
"details": "Request problems detected",
"documentation": docs,
"information": "There is a problem with your request. The request needs to adhere to certain constraints.",
},
500: map[string]interface{}{
"code": 500,
"details": "There was a problem with our servers, and we have been notified",
"documentation": docs,
},
}

30
server/json.go Normal file
View file

@ -0,0 +1,30 @@
// Copyright © 2016 Abcum Ltd
//
// 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 server
import (
"encoding/json"
)
func encode(i interface{}) interface{} {
obj, _ := json.Marshal(i)
return string(obj)
}
func decode(i interface{}) interface{} {
var obj map[string]interface{}
json.Unmarshal([]byte(i.(string)), &obj)
return obj
}

94
server/server.go Normal file
View file

@ -0,0 +1,94 @@
// Copyright © 2016 Abcum Ltd
//
// 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 server
import (
"log"
"sync"
"github.com/abcum/surreal/cnf"
"github.com/abcum/surreal/server/api"
"github.com/labstack/echo"
"github.com/labstack/echo/middleware"
)
// Setup sets up the server for rest and websockets
func Setup(ctx cnf.Context) (e error) {
var wg sync.WaitGroup
wg.Add(2)
// -------------------------------------------------------
// REST handler
// -------------------------------------------------------
r := echo.New()
r.Any("/", crud)
r.SetDebug(ctx.Verbose)
r.AutoIndex(false)
r.SetHTTPErrorHandler(errors)
r.Use(middleware.Gzip())
r.Use(middleware.Logger())
r.Use(middleware.Recover())
r.Use(api.Head(&api.HeadOpts{}))
r.Use(api.Type(&api.TypeOpts{}))
r.Use(api.Cors(&api.CorsOpts{}))
r.Use(api.Auth(&api.AuthOpts{}))
go func() {
defer wg.Done()
log.Printf("Starting HTTP server on %s", ctx.Http)
r.Run(ctx.Http)
}()
// -------------------------------------------------------
// SOCK handler
// -------------------------------------------------------
s := echo.New()
s.WebSocket("/", sock)
r.SetDebug(ctx.Verbose)
s.AutoIndex(false)
s.SetHTTPErrorHandler(errors)
s.Use(middleware.Gzip())
s.Use(middleware.Logger())
s.Use(middleware.Recover())
s.Use(api.Head(&api.HeadOpts{}))
s.Use(api.Type(&api.TypeOpts{}))
s.Use(api.Cors(&api.CorsOpts{}))
s.Use(api.Auth(&api.AuthOpts{}))
go func() {
defer wg.Done()
log.Printf("Starting SOCK server on %s", ctx.Sock)
s.Run(ctx.Sock)
}()
// -------------------------------------------------------
// Start server
// -------------------------------------------------------
wg.Wait()
return nil
}

53
server/sock.go Normal file
View file

@ -0,0 +1,53 @@
// Copyright © 2016 Abcum Ltd
//
// 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 server
import (
"github.com/abcum/surreal/sql"
"github.com/labstack/echo"
"golang.org/x/net/websocket"
)
func sock(c *echo.Context) error {
ws := c.Socket()
var msg string
for {
if err := websocket.Message.Receive(ws, &msg); err != nil {
break
}
s, e := sql.Parse(msg)
if e == nil {
if err := websocket.Message.Send(ws, encode(show(s))); err != nil {
break
}
}
if e != nil {
if err := websocket.Message.Send(ws, encode(oops(e))); err != nil {
break
}
}
}
return nil
}