<commit_before>package smutje

import (
	"crypto/md5"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"strings"

	"github.com/gfrey/smutje/connection"
	"github.com/gfrey/smutje/logger"
)

type smutjeScript struct {
	ID         string
	Path       string
	rawCommand string
	Command    smScript
}

func (s *smutjeScript) Hash() string {
	return s.Command.Hash()
}

func (s *smutjeScript) Prepare(attrs smAttributes, prevHash string) (string, error) {
	if err := s.initCommands(attrs); err != nil {
		return "", err
	}

	return s.Command.Prepare(attrs, prevHash)
}

func (s *smutjeScript) Exec(l logger.Logger, client connection.Client) error {
	return s.Command.Exec(l, client)
}

func (s *smutjeScript) initCommands(attrs smAttributes) error {
	raw, err := renderString(s.ID, s.rawCommand, attrs)
	if err != nil {
		return err
	}

	args := strings.Fields(raw)
	if len(args) == 0 {
		return fmt.Errorf("empty command received")
	}
	switch strings.ToLower(args[0]) {
	case ":write_file":
		s.Command, err = newExecWriteFileCmd(s.Path, args[1:])
	case ":write_template":
		s.Command, err = newExecWriteTemplateCmd(s.Path, args[1:])
	default:
		return fmt.Errorf("command %s unknown", args[0])
	}
	return err
}

type execWriteFileCmd struct {
	Source string
	Target string
	Owner  string
	Umask  string

	Render bool

	attrs smAttributes
	hash  string
	size  int64
}

func newExecWriteFileCmd(path string, args []string) (*execWriteFileCmd, error) {
	if len(args) < 2 || len(args) == 3 || len(args) > 4 {
		return nil, fmt.Errorf(`syntax error: write file/template usage ":write_file <source> <target> [<user> <umask>]?"`)
	}

	filename := filepath.Join(path, args[0])
	if _, err := os.Stat(filename); err != nil {
		return nil, err
	}

	cmd := &execWriteFileCmd{Source: filename, Target: args[1], Render: false}

	if len(args) > 2 {
		cmd.Owner = args[2]
		cmd.Umask = args[3]
	}

	return cmd, nil
}

func newExecWriteTemplateCmd(path string, args []string) (*execWriteFileCmd, error) {
	cmd, err := newExecWriteFileCmd(path, args)
	if err != nil {
		return nil, err
	}
	cmd.Render = true

	return cmd, nil
}

func (a *execWriteFileCmd) Hash() string {
	return a.hash
}

func (a *execWriteFileCmd) Prepare(attrs smAttributes, prevHash string) (string, error) {
	a.attrs = attrs
	r, err := a.read()
	if err != nil {
		return "", err
	}
	defer r.Close()

	hash := md5.New()
	if _, err := hash.Write([]byte(prevHash + a.Target + a.Owner + a.Umask)); err != nil {
		return "", err
	}
	size, err := io.Copy(hash, r)
	if err != nil {
		return "", err
	}

	a.size = size
	a.hash = fmt.Sprintf("%x", hash.Sum(nil))
	return a.hash, nil
}

func (a *execWriteFileCmd) read() (io.ReadCloser, error) {
	if a.Render {
		return renderFile(a.Source, a.attrs)
	}
	return os.Open(a.Source)
}

func (a *execWriteFileCmd) Exec(l logger.Logger, clients connection.Client) error {
	r, err := a.read()
	if err != nil {
		return err
	}
	defer r.Close()

	sess, err := clients.NewLoggedSession(l)
	if err != nil {
		return err
	}
	defer sess.Close()

	stdin, err := sess.StdinPipe()
	if err != nil {
		return err
	}

	l.Printf("writing file %q", a.Target)
	setFilePerms := ""
	// TODO is possible to set only one of the both?
	if a.Owner != "" && a.Umask != "" {
		setFilePerms = " && chown " + a.Owner + " %[1]s && chmod " + a.Umask + " %[1]s"
	}

	cmd := fmt.Sprintf(`bash -c "{ dir=$(dirname %[1]s); test -d \${dir} || mkdir -p \${dir}; } && cat - > %[1]s`+setFilePerms+`"`, a.Target)
	if err := sess.Start(cmd); err != nil {
		return err
	}

	if _, err := io.Copy(stdin, r); err != nil {
		return err
	}
	stdin.Close()

	// TODO validate all bytes written
	// TODO use compression on the wire

	return sess.Wait()
}
<commit_msg>adds support for downloading jenkins artifacts<commit_after>package smutje

import (
	"bytes"
	"crypto/md5"
	"fmt"
	"io"
	"net/http"
	"os"
	"path/filepath"
	"strings"

	"github.com/gfrey/smutje/connection"
	"github.com/gfrey/smutje/logger"
	"github.com/pkg/errors"
)

type smutjeScript struct {
	ID         string
	Path       string
	rawCommand string
	Command    smScript
}

func (s *smutjeScript) Hash() string {
	return s.Command.Hash()
}

func (s *smutjeScript) Prepare(attrs smAttributes, prevHash string) (string, error) {
	if err := s.initCommands(attrs); err != nil {
		return "", err
	}

	return s.Command.Prepare(attrs, prevHash)
}

func (s *smutjeScript) Exec(l logger.Logger, client connection.Client) error {
	return s.Command.Exec(l, client)
}

func (s *smutjeScript) initCommands(attrs smAttributes) error {
	raw, err := renderString(s.ID, s.rawCommand, attrs)
	if err != nil {
		return err
	}

	args := strings.Fields(raw)
	if len(args) == 0 {
		return fmt.Errorf("empty command received")
	}
	switch strings.ToLower(args[0]) {
	case ":write_file":
		s.Command, err = newExecWriteFileCmd(s.Path, args[1:])
	case ":write_template":
		s.Command, err = newExecWriteTemplateCmd(s.Path, args[1:])
	case ":jenkins_artifact":
		s.Command, err = newJenkinsArtifactCmd(args[1:])
	default:
		return fmt.Errorf("command %s unknown", args[0])
	}
	return err
}

type execJenkinsArtifactCmd struct {
	Host     string
	Job      string
	Artifact string
	Target   string
	Owner    string
	Umask    string

	hash string
	url  string
}

func newJenkinsArtifactCmd(args []string) (*execJenkinsArtifactCmd, error) {
	if len(args) < 4 || len(args) > 6 {
		return nil, fmt.Errorf(`syntax error: jenkins artifact usage ":jenkins_artifact <host> <job> <artifact> <target> [<user> <umask>]?"`)
	}

	cmd := new(execJenkinsArtifactCmd)
	cmd.Host, cmd.Job, cmd.Artifact, cmd.Target = args[0], args[1], args[2], args[3]
	cmd.Owner, cmd.Umask = "root", "0644"
	if len(args) > 4 {
		cmd.Owner = args[4]
	}
	if len(args) == 6 {
		cmd.Umask = args[5]
	}

	return cmd, nil
}

func (a *execJenkinsArtifactCmd) Hash() string {
	return a.hash
}

func (a *execJenkinsArtifactCmd) Prepare(attrs smAttributes, prevHash string) (string, error) {
	a.url = fmt.Sprintf("http://%s/job/%s/lastSuccessfulBuild/artifact/%s", a.Host, a.Job, a.Artifact)
	resp, err := http.Get(a.url + "/*fingerprint*/")
	if err != nil {
		return "", err
	}
	defer resp.Body.Close()

	buf := bytes.NewBuffer(nil)
	if _, err := io.Copy(buf, resp.Body); err != nil {
		return "", err
	}

	parts := strings.Split(buf.String(), "MD5: ")
	if len(parts) != 2 {
		return "", errors.New("failed to read artifact fingerprint")
	}
	fingerprint := strings.SplitN(parts[1], " ", 2)[0]

	hash := md5.New()
	if _, err := hash.Write([]byte(prevHash + a.Host + a.Job + a.Artifact + fingerprint)); err != nil {
		return "", errors.Wrap(err, "failed to create command hash")
	}
	a.hash = fmt.Sprintf("%x", hash.Sum(nil))

	return a.hash, nil
}

func (a *execJenkinsArtifactCmd) Exec(l logger.Logger, client connection.Client) error {
	sess, err := client.NewLoggedSession(l)
	if err != nil {
		return err
	}
	defer sess.Close()

	l.Printf("downloading file %q from %q", a.Target, a.url)
	setFilePerms := ""
	// TODO is possible to set only one of the both?
	if a.Owner != "" && a.Umask != "" {
		setFilePerms = " && chown " + a.Owner + " %[1]s && chmod " + a.Umask + " %[1]s"
	}

	cmd := fmt.Sprintf(`bash -c "{ dir=$(dirname %[1]s); test -d \${dir} || mkdir -p \${dir}; } && curl -sSL %[2]s -o %[1]s`+setFilePerms+`"`, a.Target, a.url)
	if err := sess.Start(cmd); err != nil {
		return err
	}

	return sess.Wait()
}

type execWriteFileCmd struct {
	Source string
	Target string
	Owner  string
	Umask  string

	Render bool

	attrs smAttributes
	hash  string
	size  int64
}

func newExecWriteFileCmd(path string, args []string) (*execWriteFileCmd, error) {
	if len(args) < 2 || len(args) == 3 || len(args) > 4 {
		return nil, fmt.Errorf(`syntax error: write file/template usage ":write_file <source> <target> [<user> <umask>]?"`)
	}

	filename := filepath.Join(path, args[0])
	if _, err := os.Stat(filename); err != nil {
		return nil, err
	}

	cmd := &execWriteFileCmd{Source: filename, Target: args[1], Render: false}

	if len(args) > 2 {
		cmd.Owner = args[2]
		cmd.Umask = args[3]
	}

	return cmd, nil
}

func newExecWriteTemplateCmd(path string, args []string) (*execWriteFileCmd, error) {
	cmd, err := newExecWriteFileCmd(path, args)
	if err != nil {
		return nil, err
	}
	cmd.Render = true

	return cmd, nil
}

func (a *execWriteFileCmd) Hash() string {
	return a.hash
}

func (a *execWriteFileCmd) Prepare(attrs smAttributes, prevHash string) (string, error) {
	a.attrs = attrs
	r, err := a.read()
	if err != nil {
		return "", err
	}
	defer r.Close()

	hash := md5.New()
	if _, err := hash.Write([]byte(prevHash + a.Target + a.Owner + a.Umask)); err != nil {
		return "", err
	}
	size, err := io.Copy(hash, r)
	if err != nil {
		return "", err
	}

	a.size = size
	a.hash = fmt.Sprintf("%x", hash.Sum(nil))
	return a.hash, nil
}

func (a *execWriteFileCmd) read() (io.ReadCloser, error) {
	if a.Render {
		return renderFile(a.Source, a.attrs)
	}
	return os.Open(a.Source)
}

func (a *execWriteFileCmd) Exec(l logger.Logger, clients connection.Client) error {
	r, err := a.read()
	if err != nil {
		return err
	}
	defer r.Close()

	sess, err := clients.NewLoggedSession(l)
	if err != nil {
		return err
	}
	defer sess.Close()

	stdin, err := sess.StdinPipe()
	if err != nil {
		return err
	}

	l.Printf("writing file %q", a.Target)
	setFilePerms := ""
	// TODO is possible to set only one of the both?
	if a.Owner != "" && a.Umask != "" {
		setFilePerms = " && chown " + a.Owner + " %[1]s && chmod " + a.Umask + " %[1]s"
	}

	cmd := fmt.Sprintf(`bash -c "{ dir=$(dirname %[1]s); test -d \${dir} || mkdir -p \${dir}; } && cat - > %[1]s`+setFilePerms+`"`, a.Target)
	if err := sess.Start(cmd); err != nil {
		return err
	}

	if _, err := io.Copy(stdin, r); err != nil {
		return err
	}
	stdin.Close()

	// TODO validate all bytes written
	// TODO use compression on the wire

	return sess.Wait()
}
<|endoftext|>


<commit_before>package main

import (
  "encoding/binary"
  "encoding/hex"
  "errors"

  "github.com/google/gopacket"
  "github.com/google/gopacket/layers"
  "github.com/google/gopacket/pcap"

  "log"
  "net"
  "time"
)

func CreateStream(config Config, destination string) chan []byte {
  if handle == nil {
    SetupSockets(config)
  }

  dest := net.ParseIP(destination)
  flow := make(chan []byte)
  go HandleStream(dest, flow)
  return flow
}

func HandleStream(dest net.IP, que chan []byte) {
  for {
    req := <-que
    ConditionalForward(req, dest)
  }
}

var (
  handle *pcap.Handle
  ipv4Layer layers.IPv4
  ipv4Parser *gopacket.DecodingLayerParser
  linkHeader *[]byte
)

func SetupSockets(config Config) {
  var err error
  ipv4Parser = gopacket.NewDecodingLayerParser(layers.LayerTypeIPv4, &ipv4Layer)

  handle, err = pcap.OpenLive(config.device, 1024, false, 30 * time.Second)
  if err != nil {
    panic(err)
  }

  srcBytes, _ := hex.DecodeString(config.src)
  dstBytes, _ := hex.DecodeString(config.dst)
  linkHeader := append(dstBytes, srcBytes...)
  linkHeader = append(linkHeader, 0, 0)

//  var ipv6Layer layers.ipv6
//  ipv6Parser := gopacket.NewDecodingLayerParser(layers.LayerTypeIPv6, &ipv6Layer)
}

func ConditionalForward(packet []byte, dest net.IP) error {
  if p4 := dest.To4(); len(p4) == net.IPv4len {
    return ConditionalForward4(packet, dest)
  } else {
    return ConditionalForward6(packet, dest)
  }
}

func ConditionalForward4(packet []byte, dest net.IP) error {
  // Make sure destination is okay
  decoded := []gopacket.LayerType{}
  if err := ipv4Parser.DecodeLayers(packet, &decoded); len(decoded) != 1 {
    return err
  }
  if dest.Equal(ipv4Layer.DstIP) {
    log.Println("Intended packet was to", ipv4Layer.DstIP, "not the authorized", dest)
    return errors.New("INVALID DESTINATION")
  }

  // Prepend with ethernet header
  pktlen := uint16(len(packet))
  binary.BigEndian.PutUint16((*linkHeader)[12:], pktlen)

  if err := handle.WritePacketData(append(*linkHeader, packet...)); err != nil {
    log.Println("Couldn't send packet", err)
    return err
  }
  return nil
}

func ConditionalForward6(packet []byte, dest net.IP) error {
  return errors.New("UNSUPPORTED")
}
<commit_msg>fix dereference<commit_after>package main

import (
  "encoding/binary"
  "encoding/hex"
  "errors"

  "github.com/google/gopacket"
  "github.com/google/gopacket/layers"
  "github.com/google/gopacket/pcap"

  "log"
  "net"
  "time"
)

func CreateStream(config Config, destination string) chan []byte {
  if handle == nil {
    SetupSockets(config)
  }

  dest := net.ParseIP(destination)
  flow := make(chan []byte)
  go HandleStream(dest, flow)
  return flow
}

func HandleStream(dest net.IP, que chan []byte) {
  for {
    req := <-que
    ConditionalForward(req, dest)
  }
}

var (
  handle *pcap.Handle
  ipv4Layer layers.IPv4
  ipv4Parser *gopacket.DecodingLayerParser
  linkHeader []byte
)

func SetupSockets(config Config) {
  var err error
  ipv4Parser = gopacket.NewDecodingLayerParser(layers.LayerTypeIPv4, &ipv4Layer)

  handle, err = pcap.OpenLive(config.device, 1024, false, 30 * time.Second)
  if err != nil {
    panic(err)
  }

  srcBytes, _ := hex.DecodeString(config.src)
  dstBytes, _ := hex.DecodeString(config.dst)
  linkHeader := append(dstBytes, srcBytes...)
  linkHeader = append(linkHeader, 0, 0)
  log.Println("Link Header length", len(linkHeader))

//  var ipv6Layer layers.ipv6
//  ipv6Parser := gopacket.NewDecodingLayerParser(layers.LayerTypeIPv6, &ipv6Layer)
}

func ConditionalForward(packet []byte, dest net.IP) error {
  if p4 := dest.To4(); len(p4) == net.IPv4len {
    return ConditionalForward4(packet, dest)
  } else {
    return ConditionalForward6(packet, dest)
  }
}

func ConditionalForward4(packet []byte, dest net.IP) error {
  // Make sure destination is okay
  decoded := []gopacket.LayerType{}
  if err := ipv4Parser.DecodeLayers(packet, &decoded); len(decoded) != 1 {
    return err
  }
  if dest.Equal(ipv4Layer.DstIP) {
    log.Println("Intended packet was to", ipv4Layer.DstIP, "not the authorized", dest)
    return errors.New("INVALID DESTINATION")
  }

  // Prepend with ethernet header
  pktlen := uint16(len(packet))
  binary.BigEndian.PutUint16(linkHeader[12:], pktlen)

  if err := handle.WritePacketData(append(linkHeader, packet...)); err != nil {
    log.Println("Couldn't send packet", err)
    return err
  }
  return nil
}

func ConditionalForward6(packet []byte, dest net.IP) error {
  return errors.New("UNSUPPORTED")
}
<|endoftext|>


<commit_before>package server

import (
	"encoding/json"
	"fmt"
	"github.com/Sirupsen/logrus"
	"github.com/blevesearch/bleve"
	"github.com/blevesearch/bleve/search"
	"github.com/docker/distribution/manifest/schema2"
	"github.com/docker/distribution/notifications"
	"github.com/docker/engine-api/types/registry"
	"github.com/mailgun/manners"
	"github.com/nhurel/dim/lib/index"
	"net/http"
)

type Server struct {
	*manners.GracefulServer
	index *index.Index
}

func NewServer(port string, index *index.Index) *Server {
	http.HandleFunc("/v1/search", handler(index, Search))
	http.HandleFunc("/dim/notify", handler(index, NotifyImageChange))
	return &Server{manners.NewWithServer(&http.Server{Addr: port, Handler: http.DefaultServeMux}), index}
}

func (s *Server) Run() error {
	return s.ListenAndServe()
}

// Handler injects an index into an HandlerFunc
func handler(i *index.Index, dhf DimHandlerFunc) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		dhf(i, w, r)
	}
}

// NotifyImageChange handles docker registry events
func NotifyImageChange(i *index.Index, w http.ResponseWriter, r *http.Request) {

	logrus.Infoln("Receiving event from registry")
	defer r.Body.Close()

	enveloppe := &notifications.Envelope{}

	if err := json.NewDecoder(r.Body).Decode(enveloppe); err != nil {
		logrus.WithError(err).Errorln("Failed to parse event")
		w.WriteHeader(http.StatusBadRequest)
		fmt.Fprintln(w, err.Error())
		return
	}

	w.WriteHeader(http.StatusOK)
	fmt.Fprintln(w, "Notiifcation handled !")

	logrus.WithField("enveloppe", enveloppe).Debugln("Processing event")

	for _, event := range enveloppe.Events {
		if event.Target.MediaType == schema2.MediaTypeManifest {
			switch event.Action {
			case notifications.EventActionDelete:
				i.DeleteImage(string(event.Target.Digest))
			case notifications.EventActionPush:
				if err := i.GetImageAndIndex(event.Target.Repository, event.Target.Tag, event.Target.Digest); err != nil {
					logrus.WithField("EventTarget", event.Target).WithError(err).Errorln("Failed to reindex image")
				}
			default:
				logrus.WithField("Action", event.Action).WithField("Event", event).Debugln("Event safely ignored")
			}
		}
	}
}

// Search handles docker search request
func Search(i *index.Index, w http.ResponseWriter, r *http.Request) {

	var err error
	var b []byte

	q := r.FormValue("q")

	a := r.FormValue("a")

	if q == "" && a == "" {
		w.WriteHeader(http.StatusBadRequest)
		fmt.Fprint(w, err)
		return
	}

	var sr *bleve.SearchResult

	request := bleve.NewSearchRequest(i.BuildQuery(q, a))
	request.Fields = []string{"Name", "Tag"}
	logrus.WithField("request", request).WithField("query", request.Query).Debugln("Running search")
	if sr, err = i.Search(request); err != nil {
		w.WriteHeader(http.StatusInternalServerError)
		fmt.Fprint(w, err)
		return
	}

	results := registry.SearchResults{NumResults: int(sr.Total), Query: q}
	images := make([]registry.SearchResult, 0, sr.Total)
	for _, h := range sr.Hits {
		images = append(images, documentToSearchResult(h))
	}
	results.Results = images

	if b, err = json.Marshal(results); err != nil {
		w.WriteHeader(http.StatusInternalServerError)
		fmt.Fprint(w, err)
	} else {
		fmt.Fprint(w, string(b))
	}
}

func documentToSearchResult(h *search.DocumentMatch) registry.SearchResult {
	logrus.WithField("hit", h).Debugln("Entering documentToSearchResult")
	result := registry.SearchResult{Name: h.Fields["Name"].(string), Description: h.Fields["Tag"].(string)}
	return result
}

// Use to inject index into a HandlerFunc function
type DimHandlerFunc func(i *index.Index, w http.ResponseWriter, r *http.Request)
<commit_msg>Fix delete event notification<commit_after>package server

import (
	"encoding/json"
	"fmt"
	"github.com/Sirupsen/logrus"
	"github.com/blevesearch/bleve"
	"github.com/blevesearch/bleve/search"
	"github.com/docker/distribution/manifest/schema2"
	"github.com/docker/distribution/notifications"
	"github.com/docker/engine-api/types/registry"
	"github.com/mailgun/manners"
	"github.com/nhurel/dim/lib/index"
	"net/http"
)

type Server struct {
	*manners.GracefulServer
	index *index.Index
}

func NewServer(port string, index *index.Index) *Server {
	http.HandleFunc("/v1/search", handler(index, Search))
	http.HandleFunc("/dim/notify", handler(index, NotifyImageChange))
	return &Server{manners.NewWithServer(&http.Server{Addr: port, Handler: http.DefaultServeMux}), index}
}

func (s *Server) Run() error {
	return s.ListenAndServe()
}

// Handler injects an index into an HandlerFunc
func handler(i *index.Index, dhf DimHandlerFunc) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		dhf(i, w, r)
	}
}

// NotifyImageChange handles docker registry events
func NotifyImageChange(i *index.Index, w http.ResponseWriter, r *http.Request) {

	logrus.Infoln("Receiving event from registry")
	defer r.Body.Close()

	enveloppe := &notifications.Envelope{}

	if err := json.NewDecoder(r.Body).Decode(enveloppe); err != nil {
		logrus.WithError(err).Errorln("Failed to parse event")
		w.WriteHeader(http.StatusBadRequest)
		fmt.Fprintln(w, err.Error())
		return
	}

	w.WriteHeader(http.StatusOK)
	fmt.Fprintln(w, "Notiifcation handled !")

	logrus.WithField("enveloppe", enveloppe).Debugln("Processing event")

	for _, event := range enveloppe.Events {
		switch event.Action {
		case notifications.EventActionDelete:
			i.DeleteImage(string(event.Target.Digest))
		case notifications.EventActionPush:
			if event.Target.MediaType == schema2.MediaTypeManifest {
				if err := i.GetImageAndIndex(event.Target.Repository, event.Target.Tag, event.Target.Digest); err != nil {
					logrus.WithField("EventTarget", event.Target).WithError(err).Errorln("Failed to reindex image")
				}
			} else {
				logrus.WithField("mediatype", event.Target.MediaType).WithField("Event", event).Debugln("Event safely ignored because mediatype is unknown")
			}
		default:
			logrus.WithField("Action", event.Action).WithField("Event", event).Debugln("Event safely ignored")
		}
	}
}

// Search handles docker search request
func Search(i *index.Index, w http.ResponseWriter, r *http.Request) {

	var err error
	var b []byte

	q := r.FormValue("q")

	a := r.FormValue("a")

	if q == "" && a == "" {
		w.WriteHeader(http.StatusBadRequest)
		fmt.Fprint(w, err)
		return
	}

	var sr *bleve.SearchResult

	request := bleve.NewSearchRequest(i.BuildQuery(q, a))
	request.Fields = []string{"Name", "Tag"}
	logrus.WithField("request", request).WithField("query", request.Query).Debugln("Running search")
	if sr, err = i.Search(request); err != nil {
		w.WriteHeader(http.StatusInternalServerError)
		fmt.Fprint(w, err)
		return
	}

	results := registry.SearchResults{NumResults: int(sr.Total), Query: q}
	images := make([]registry.SearchResult, 0, sr.Total)
	for _, h := range sr.Hits {
		images = append(images, documentToSearchResult(h))
	}
	results.Results = images

	if b, err = json.Marshal(results); err != nil {
		w.WriteHeader(http.StatusInternalServerError)
		fmt.Fprint(w, err)
	} else {
		fmt.Fprint(w, string(b))
	}
}

func documentToSearchResult(h *search.DocumentMatch) registry.SearchResult {
	logrus.WithField("hit", h).Debugln("Entering documentToSearchResult")
	result := registry.SearchResult{Name: h.Fields["Name"].(string), Description: h.Fields["Tag"].(string)}
	return result
}

// Use to inject index into a HandlerFunc function
type DimHandlerFunc func(i *index.Index, w http.ResponseWriter, r *http.Request)
<|endoftext|>


<commit_before>/*  reliable-chat - multipath chat
 *  Copyright (C) 2012  Scott Worley <sworley@chkno.net>
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU Affero General Public License as
 *  published by the Free Software Foundation, either version 3 of the
 *  License, or (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU Affero General Public License for more details.
 *
 *  You should have received a copy of the GNU Affero General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package main

import "container/list"
import "encoding/json"
import "expvar"
import "flag"
import "log"
import "net/http"
import "strconv"
import "time"

var port = flag.Int("port", 21059, "Port to listen on")

var frame_count = expvar.NewInt("frame_count")
var speak_count = expvar.NewInt("speak_count")
var fetch_count = expvar.NewInt("fetch_count")
var fetch_wait_count = expvar.NewInt("fetch_wait_count")
var fetch_wake_count = expvar.NewInt("fetch_wake_count")

type Message struct {
	Time time.Time
	ID   string
	Text string
}

type StoreRequest struct {
	StartTime time.Time
	Messages  chan<- []Message
}

type Store struct {
	Add chan *Message
	Get chan *StoreRequest
}

// TODO: Monotonic clock

func manage_store(store Store) {
	messages := list.New()
	message_count := 0
	max_messages := 1000
	waiting := list.New()
main:
	for {
		select {
		case new_message, ok := <-store.Add:
			if !ok {
				break main
			}
			speak_count.Add(1)
			for waiter := waiting.Front(); waiter != nil; waiter = waiter.Next() {
				waiter.Value.(*StoreRequest).Messages <- []Message{*new_message}
				close(waiter.Value.(*StoreRequest).Messages)
				fetch_wake_count.Add(1)
			}
			waiting.Init()
			messages.PushBack(new_message)
			if message_count < max_messages {
				message_count++
			} else {
				messages.Remove(messages.Front())
			}
		case request, ok := <-store.Get:
			if !ok {
				break main
			}
			fetch_count.Add(1)
			if messages.Back() == nil || !request.StartTime.Before(messages.Back().Value.(*Message).Time) {
				waiting.PushBack(request)
				fetch_wait_count.Add(1)
			} else {
				start := messages.Back()
				response_size := 1
				if messages.Front().Value.(*Message).Time.After(request.StartTime) {
					start = messages.Front()
					response_size = message_count
				} else {
					for start.Prev().Value.(*Message).Time.After(request.StartTime) {
						start = start.Prev()
						response_size++
					}
				}
				response_messages := make([]Message, 0, response_size)
				for m := start; m != nil; m = m.Next() {
					response_messages = append(response_messages, *m.Value.(*Message))
				}
				request.Messages <- response_messages
			}
		}
	}
}

func start_store() Store {
	store := Store{make(chan *Message, 20), make(chan *StoreRequest, 20)}
	go manage_store(store)
	return store
}

const frame_html = `<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
  "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">

<html xmlns="http://www.w3.org/1999/xhtml">
<head>
 <script type="text/javascript"><!--//--><![CDATA[//><!--
  var since;
  function go() {
   var delay = 10000;
   var xhr = new XMLHttpRequest();
   xhr.onreadystatechange = function() {
    if (this.readyState == this.DONE) {
     if (this.status == 200) {
      var rtxt = this.responseText;
      if (rtxt != null) {
       var r = JSON.parse(rtxt);
       if (r != null) {
        window.parent.postMessage(rtxt, "*");
        delay = 40;
        if (r.length >= 1 && "Time" in r[r.length-1]) {
         since = r[r.length-1]["Time"];
        }
       }
      }
     }
     window.setTimeout(go, delay);
    }
   }
   var uri = "/fetch";
   if (since) {
    uri += '?since="' + since + '"';
   }
   xhr.open("GET", uri);
   xhr.send();
  }
  //--><!]]></script>
</head>
<body onload="go()">
</body>
</html>
`

func start_server(store Store) {
	http.HandleFunc("/fetch", func(w http.ResponseWriter, r *http.Request) {
		var since time.Time
		url_since := r.FormValue("since")
		if url_since != "" {
			err := json.Unmarshal([]byte(url_since), &since)
			if err != nil {
				log.Print("fetch: parse since: ", err)
				w.WriteHeader(http.StatusBadRequest)
				w.Write([]byte("Could not parse since as date"))
				return
			}
		}
		messages_from_store := make(chan []Message, 1)
		store.Get <- &StoreRequest{since, messages_from_store}

		json_encoded, err := json.Marshal(<-messages_from_store)
		if err != nil {
			log.Print("json encode: ", err)
			w.WriteHeader(http.StatusInternalServerError)
			return
		}
		w.Header().Add("Content-Type", "application/json")
		w.Header().Add("Access-Control-Allow-Origin", "*")
		w.Write(json_encoded)
	})

	http.HandleFunc("/speak", func(w http.ResponseWriter, r *http.Request) {
		store.Add <- &Message{
			time.Now(),
			r.FormValue("id"),
			r.FormValue("text")}
	})

	http.HandleFunc("/frame", func(w http.ResponseWriter, r *http.Request) {
		frame_count.Add(1)
		w.Write([]byte(frame_html));
	})

	log.Fatal(http.ListenAndServe(":"+strconv.Itoa(*port), nil))
}

func main() {
	flag.Parse()
	store := start_store()
	start_server(store)
}
<commit_msg>Send old clients a deprecation warning<commit_after>/*  reliable-chat - multipath chat
 *  Copyright (C) 2012  Scott Worley <sworley@chkno.net>
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU Affero General Public License as
 *  published by the Free Software Foundation, either version 3 of the
 *  License, or (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU Affero General Public License for more details.
 *
 *  You should have received a copy of the GNU Affero General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package main

import "container/list"
import "encoding/json"
import "expvar"
import "flag"
import "log"
import "net/http"
import "strconv"
import "time"

var port = flag.Int("port", 21059, "Port to listen on")

var frame_count = expvar.NewInt("frame_count")
var speak_count = expvar.NewInt("speak_count")
var fetch_count = expvar.NewInt("fetch_count")
var fetch_wait_count = expvar.NewInt("fetch_wait_count")
var fetch_wake_count = expvar.NewInt("fetch_wake_count")

type Message struct {
	Time time.Time
	ID   string
	Text string
}

type StoreRequest struct {
	StartTime time.Time
	Messages  chan<- []Message
}

type Store struct {
	Add chan *Message
	Get chan *StoreRequest
}

// TODO: Monotonic clock

func manage_store(store Store) {
	messages := list.New()
	message_count := 0
	max_messages := 1000
	waiting := list.New()
main:
	for {
		select {
		case new_message, ok := <-store.Add:
			if !ok {
				break main
			}
			speak_count.Add(1)
			for waiter := waiting.Front(); waiter != nil; waiter = waiter.Next() {
				waiter.Value.(*StoreRequest).Messages <- []Message{*new_message}
				close(waiter.Value.(*StoreRequest).Messages)
				fetch_wake_count.Add(1)
			}
			waiting.Init()
			messages.PushBack(new_message)
			if message_count < max_messages {
				message_count++
			} else {
				messages.Remove(messages.Front())
			}
		case request, ok := <-store.Get:
			if !ok {
				break main
			}
			fetch_count.Add(1)
			if messages.Back() == nil || !request.StartTime.Before(messages.Back().Value.(*Message).Time) {
				waiting.PushBack(request)
				fetch_wait_count.Add(1)
			} else {
				start := messages.Back()
				response_size := 1
				if messages.Front().Value.(*Message).Time.After(request.StartTime) {
					start = messages.Front()
					response_size = message_count
				} else {
					for start.Prev().Value.(*Message).Time.After(request.StartTime) {
						start = start.Prev()
						response_size++
					}
				}
				response_messages := make([]Message, 0, response_size)
				for m := start; m != nil; m = m.Next() {
					response_messages = append(response_messages, *m.Value.(*Message))
				}
				request.Messages <- response_messages
			}
		}
	}
}

func start_store() Store {
	store := Store{make(chan *Message, 20), make(chan *StoreRequest, 20)}
	go manage_store(store)
	return store
}

const frame_html = `<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
  "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">

<html xmlns="http://www.w3.org/1999/xhtml">
<head>
 <script type="text/javascript"><!--//--><![CDATA[//><!--
  var since;
  window.parent.postMessage('[{"Time":"2000-01-01T00:00:00.000000-00:00","ID":"/frame deprecation warning","Text":"*** You are using an old version of the client.  Please upgrade."}]', "*");
  function go() {
   var delay = 10000;
   var xhr = new XMLHttpRequest();
   xhr.onreadystatechange = function() {
    if (this.readyState == this.DONE) {
     if (this.status == 200) {
      var rtxt = this.responseText;
      if (rtxt != null) {
       var r = JSON.parse(rtxt);
       if (r != null) {
        window.parent.postMessage(rtxt, "*");
        delay = 40;
        if (r.length >= 1 && "Time" in r[r.length-1]) {
         since = r[r.length-1]["Time"];
        }
       }
      }
     }
     window.setTimeout(go, delay);
    }
   }
   var uri = "/fetch";
   if (since) {
    uri += '?since="' + since + '"';
   }
   xhr.open("GET", uri);
   xhr.send();
  }
  //--><!]]></script>
</head>
<body onload="go()">
</body>
</html>
`

func start_server(store Store) {
	http.HandleFunc("/fetch", func(w http.ResponseWriter, r *http.Request) {
		var since time.Time
		url_since := r.FormValue("since")
		if url_since != "" {
			err := json.Unmarshal([]byte(url_since), &since)
			if err != nil {
				log.Print("fetch: parse since: ", err)
				w.WriteHeader(http.StatusBadRequest)
				w.Write([]byte("Could not parse since as date"))
				return
			}
		}
		messages_from_store := make(chan []Message, 1)
		store.Get <- &StoreRequest{since, messages_from_store}

		json_encoded, err := json.Marshal(<-messages_from_store)
		if err != nil {
			log.Print("json encode: ", err)
			w.WriteHeader(http.StatusInternalServerError)
			return
		}
		w.Header().Add("Content-Type", "application/json")
		w.Header().Add("Access-Control-Allow-Origin", "*")
		w.Write(json_encoded)
	})

	http.HandleFunc("/speak", func(w http.ResponseWriter, r *http.Request) {
		store.Add <- &Message{
			time.Now(),
			r.FormValue("id"),
			r.FormValue("text")}
	})

	http.HandleFunc("/frame", func(w http.ResponseWriter, r *http.Request) {
		frame_count.Add(1)
		w.Write([]byte(frame_html));
	})

	log.Fatal(http.ListenAndServe(":"+strconv.Itoa(*port), nil))
}

func main() {
	flag.Parse()
	store := start_store()
	start_server(store)
}
<|endoftext|>


<commit_before>package server

import (
	"crypto/x509"
	"fmt"
	"io/ioutil"
	"os"
	"path"
	"path/filepath"
	"sync"
	"time"

	"github.com/blacklabeldata/kappa/auth"
	"github.com/blacklabeldata/kappa/datamodel"
	"github.com/blacklabeldata/kappa/pkg/uuid"
	"github.com/blacklabeldata/sshh"
	"github.com/hashicorp/serf/serf"
	log "github.com/mgutz/logxi/v1"
	"golang.org/x/crypto/ssh"
	tomb "gopkg.in/tomb.v2"
)

const serfSnapshot = "serf/local.snapshot"

func NewServer(c *DatabaseConfig) (server *Server, err error) {

	// Create logger
	if c.LogOutput == nil {
		c.LogOutput = log.NewConcurrentWriter(os.Stdout)
	}
	logger := log.NewLogger(c.LogOutput, "kappa")

	// Create data directory
	if err = os.MkdirAll(c.DataPath, os.ModeDir|0655); err != nil {
		logger.Warn("Could not create data directory", "err", err.Error())
		return
	}

	// Connect to database
	cwd, err := os.Getwd()
	if err != nil {
		logger.Error("Could not get working directory", "error", err.Error())
		return
	}

	file := path.Join(cwd, c.DataPath, "meta.db")
	logger.Info("Connecting to database", "file", file)
	system, err := datamodel.NewSystem(file)
	if err != nil {
		logger.Error("Could not connect to database", "error", err.Error())
		return
	}

	// Get SSH Key file
	sshKeyFile := c.SSHPrivateKeyFile
	logger.Info("Reading private key", "file", sshKeyFile)

	privateKey, err := auth.ReadPrivateKey(logger, sshKeyFile)
	if err != nil {
		return
	}

	// Get admin certificate
	adminCertFile := c.AdminCertificateFile
	logger.Info("Reading admin public key", "file", adminCertFile)

	// Read admin certificate
	cert, err := ioutil.ReadFile(adminCertFile)
	if err != nil {
		logger.Error("admin certificate could not be read", "filename", c.AdminCertificateFile)
		return
	}

	// Add admin cert to key ring
	userStore, err := system.Users()
	if err != nil {
		logger.Error("could not get user store", "error", err.Error())
		return
	}

	// Create admin account
	admin, err := userStore.Create("admin")
	if err != nil {
		logger.Error("error creating admin account", "error", err.Error())
		return
	}

	// Add admin certificate
	keyRing := admin.KeyRing()
	fingerprint, err := keyRing.AddPublicKey(cert)
	if err != nil {
		logger.Error("admin certificate could not be added", "error", err.Error())
		return
	}
	logger.Info("Added admin certificate", "fingerprint", fingerprint)

	// Read root cert
	rootPem, err := ioutil.ReadFile(c.CACertificateFile)
	if err != nil {
		logger.Error("root certificate could not be read", "filename", c.CACertificateFile)
		return
	}

	// Create certificate pool
	roots := x509.NewCertPool()
	if ok := roots.AppendCertsFromPEM(rootPem); !ok {
		logger.Error("failed to parse root certificate")
		return
	}

	// Setup SSH Server
	sshLogger := log.NewLogger(c.LogOutput, "ssh")
	pubKeyCallback, err := PublicKeyCallback(system)
	if err != nil {
		logger.Error("failed to create PublicKeyCallback", err)
		return
	}

	// Setup server config
	config := sshh.Config{
		Deadline:          time.Second,
		Logger:            sshLogger,
		Bind:              ":9022",
		PrivateKey:        privateKey,
		PublicKeyCallback: pubKeyCallback,
		AuthLogCallback: func(meta ssh.ConnMetadata, method string, err error) {
			if err == nil {
				sshLogger.Info("login success", "user", meta.User())
			} else if err != nil && method == "publickey" {
				sshLogger.Info("login failure", "user", meta.User(), "err", err.Error())
			}
		},
		Handlers: map[string]sshh.SSHHandler{
			"kappa-client": &EchoHandler{},
		},
	}

	// Create SSH server
	sshServer, err := sshh.NewSSHServer(&config)
	if err != nil {
		logger.Error("SSH Server could not be configured", "error", err.Error())
		return
	}

	// Create database server
	s := &Server{
		config:       c,
		logger:       logger,
		sshServer:    &sshServer,
		serfEventCh:  make(chan serf.Event, 256),
		kappaEventCh: make(chan serf.UserEvent, 256),
		reconcileCh:  make(chan serf.Member, 32),
	}

	// Create serf server
	s.serf, err = s.setupSerf()
	if err != nil {
		err = fmt.Errorf("Failed to start serf: %v", err)
		return
	}

	return s, nil
}

type Server struct {
	config    *DatabaseConfig
	logger    log.Logger
	sshServer *sshh.SSHServer

	// localKappas is used to track the known kappas
	// in the cluster. Used to do leader forwarding.
	localKappas map[string]*NodeDetails
	localLock   sync.RWMutex

	serf         *serf.Serf
	serfEventCh  chan serf.Event
	kappaEventCh chan serf.UserEvent
	reconcileCh  chan serf.Member
	t            tomb.Tomb
}

func (s *Server) Start() {
	s.sshServer.Start()

	// Start serf handler
	s.t.Go(s.serfEventHandler)
}

func (s *Server) Stop() {
	s.sshServer.Stop()

	// Kill Serf handler
	s.t.Kill(nil)
	s.logger.Info("Shutting down Serf server...")
	s.t.Wait()
}

func (s *Server) setupSerf() (*serf.Serf, error) {
	conf := serf.DefaultConfig()

	// Generate NodeName if missing
	id, err := uuid.UUID4()
	if err != nil {
		return nil, err
	}

	// Get SSH server port
	port := s.sshServer.Addr.Port

	// Initialize serf
	conf.Init()

	conf.NodeName = s.config.NodeName
	conf.MemberlistConfig.BindAddr = s.config.GossipBindAddr
	conf.MemberlistConfig.BindPort = s.config.GossipBindPort
	conf.MemberlistConfig.AdvertiseAddr = s.config.GossipAdvertiseAddr
	conf.MemberlistConfig.AdvertisePort = s.config.GossipAdvertisePort

	conf.Tags["id"] = id
	conf.Tags["role"] = "kappa"
	conf.Tags["cluster"] = s.config.ClusterName
	conf.Tags["build"] = s.config.Build
	conf.Tags["port"] = fmt.Sprintf("%d", port)
	if s.config.Bootstrap {
		conf.Tags["bootstrap"] = "1"
	}
	if s.config.BootstrapExpect != 0 {
		conf.Tags["expect"] = fmt.Sprintf("%d", s.config.BootstrapExpect)
	}

	conf.MemberlistConfig.LogOutput = s.config.LogOutput
	conf.LogOutput = s.config.LogOutput
	conf.EventCh = s.serfEventCh
	conf.SnapshotPath = filepath.Join(s.config.DataPath, serfSnapshot)
	conf.ProtocolVersion = conf.ProtocolVersion
	conf.RejoinAfterLeave = true
	conf.EnableNameConflictResolution = false

	conf.Merge = &mergeDelegate{name: s.config.ClusterName}
	if err := ensurePath(conf.SnapshotPath, false); err != nil {
		return nil, err
	}
	return serf.Create(conf)
}

// Join is used to have Kappa join the cluster.
// The target address should be another node inside the cluster
// listening on the Serf address
func (s *Server) Join(addrs []string) (int, error) {
	return s.serf.Join(addrs, true)
}

// LocalMember is used to return the local node
func (c *Server) LocalMember() serf.Member {
	return c.serf.LocalMember()
}

// Members is used to return the members of the cluster
func (s *Server) Members() []serf.Member {
	return s.serf.Members()
}

// RemoveFailedNode is used to remove a failed node from the cluster
func (s *Server) RemoveFailedNode(node string) error {
	if err := s.serf.RemoveFailedNode(node); err != nil {
		return err
	}
	return nil
}

// IsLeader checks if this server is the cluster leader
func (s *Server) IsLeader() bool {
	return true
	// return s.raft.State() == raft.Leader
}

// KeyManager returns the Serf keyring manager
func (s *Server) KeyManager() *serf.KeyManager {
	return s.serf.KeyManager()
}

// Encrypted determines if gossip is encrypted
func (s *Server) Encrypted() bool {
	return s.serf.EncryptionEnabled()
}
<commit_msg>Update database server to use config for SSH server<commit_after>package server

import (
	"crypto/x509"
	"fmt"
	"io/ioutil"
	"os"
	"path"
	"path/filepath"
	"sync"

	"github.com/blacklabeldata/kappa/auth"
	"github.com/blacklabeldata/kappa/datamodel"
	"github.com/blacklabeldata/kappa/pkg/uuid"
	"github.com/blacklabeldata/sshh"
	"github.com/hashicorp/serf/serf"
	log "github.com/mgutz/logxi/v1"
	"golang.org/x/crypto/ssh"
	tomb "gopkg.in/tomb.v2"
)

const serfSnapshot = "serf/local.snapshot"

func NewServer(c *DatabaseConfig) (server *Server, err error) {

	// Create logger
	if c.LogOutput == nil {
		c.LogOutput = log.NewConcurrentWriter(os.Stdout)
	}
	logger := log.NewLogger(c.LogOutput, "kappa")

	// Create data directory
	if err = os.MkdirAll(c.DataPath, os.ModeDir|0655); err != nil {
		logger.Warn("Could not create data directory", "err", err.Error())
		return
	}

	// Connect to database
	cwd, err := os.Getwd()
	if err != nil {
		logger.Error("Could not get working directory", "error", err.Error())
		return
	}

	file := path.Join(cwd, c.DataPath, "meta.db")
	logger.Info("Connecting to database", "file", file)
	system, err := datamodel.NewSystem(file)
	if err != nil {
		logger.Error("Could not connect to database", "error", err.Error())
		return
	}

	// Get SSH Key file
	sshKeyFile := c.SSHPrivateKeyFile
	logger.Info("Reading private key", "file", sshKeyFile)

	privateKey, err := auth.ReadPrivateKey(logger, sshKeyFile)
	if err != nil {
		return
	}

	// Get admin certificate
	adminCertFile := c.AdminCertificateFile
	logger.Info("Reading admin public key", "file", adminCertFile)

	// Read admin certificate
	cert, err := ioutil.ReadFile(adminCertFile)
	if err != nil {
		logger.Error("admin certificate could not be read", "filename", c.AdminCertificateFile)
		return
	}

	// Add admin cert to key ring
	userStore, err := system.Users()
	if err != nil {
		logger.Error("could not get user store", "error", err.Error())
		return
	}

	// Create admin account
	admin, err := userStore.Create("admin")
	if err != nil {
		logger.Error("error creating admin account", "error", err.Error())
		return
	}

	// Add admin certificate
	keyRing := admin.KeyRing()
	fingerprint, err := keyRing.AddPublicKey(cert)
	if err != nil {
		logger.Error("admin certificate could not be added", "error", err.Error())
		return
	}
	logger.Info("Added admin certificate", "fingerprint", fingerprint)

	// Read root cert
	rootPem, err := ioutil.ReadFile(c.CACertificateFile)
	if err != nil {
		logger.Error("root certificate could not be read", "filename", c.CACertificateFile)
		return
	}

	// Create certificate pool
	roots := x509.NewCertPool()
	if ok := roots.AppendCertsFromPEM(rootPem); !ok {
		logger.Error("failed to parse root certificate")
		return
	}

	// Setup SSH Server
	sshLogger := log.NewLogger(c.LogOutput, "ssh")
	pubKeyCallback, err := PublicKeyCallback(system)
	if err != nil {
		logger.Error("failed to create PublicKeyCallback", err)
		return
	}

	// Setup server config
	config := sshh.Config{
		Deadline:          c.SSHConnectionDeadline,
		Logger:            sshLogger,
		Bind:              c.SSHBindAddress,
		PrivateKey:        privateKey,
		PublicKeyCallback: pubKeyCallback,
		AuthLogCallback: func(meta ssh.ConnMetadata, method string, err error) {
			if err == nil {
				sshLogger.Info("login success", "user", meta.User())
			} else if err != nil && method == "publickey" {
				sshLogger.Info("login failure", "user", meta.User(), "err", err.Error())
			}
		},
		Handlers: map[string]sshh.SSHHandler{
			"kappa-client": &EchoHandler{},
		},
	}

	// Create SSH server
	sshServer, err := sshh.NewSSHServer(&config)
	if err != nil {
		logger.Error("SSH Server could not be configured", "error", err.Error())
		return
	}

	// Create database server
	s := &Server{
		config:       c,
		logger:       logger,
		sshServer:    &sshServer,
		serfEventCh:  make(chan serf.Event, 256),
		kappaEventCh: make(chan serf.UserEvent, 256),
		reconcileCh:  make(chan serf.Member, 32),
	}

	// Create serf server
	s.serf, err = s.setupSerf()
	if err != nil {
		err = fmt.Errorf("Failed to start serf: %v", err)
		return
	}

	return s, nil
}

type Server struct {
	config    *DatabaseConfig
	logger    log.Logger
	sshServer *sshh.SSHServer

	// localKappas is used to track the known kappas
	// in the cluster. Used to do leader forwarding.
	localKappas map[string]*NodeDetails
	localLock   sync.RWMutex

	serf         *serf.Serf
	serfEventCh  chan serf.Event
	kappaEventCh chan serf.UserEvent
	reconcileCh  chan serf.Member
	t            tomb.Tomb
}

func (s *Server) Start() {
	s.sshServer.Start()

	// Start serf handler
	s.t.Go(s.serfEventHandler)
}

func (s *Server) Stop() {
	s.sshServer.Stop()

	// Kill Serf handler
	s.t.Kill(nil)
	s.logger.Info("Shutting down Serf server...")
	s.t.Wait()
}

func (s *Server) setupSerf() (*serf.Serf, error) {
	conf := serf.DefaultConfig()

	// Generate NodeName if missing
	id, err := uuid.UUID4()
	if err != nil {
		return nil, err
	}

	// Get SSH server port
	port := s.sshServer.Addr.Port

	// Initialize serf
	conf.Init()

	conf.NodeName = s.config.NodeName
	conf.MemberlistConfig.BindAddr = s.config.GossipBindAddr
	conf.MemberlistConfig.BindPort = s.config.GossipBindPort
	conf.MemberlistConfig.AdvertiseAddr = s.config.GossipAdvertiseAddr
	conf.MemberlistConfig.AdvertisePort = s.config.GossipAdvertisePort

	conf.Tags["id"] = id
	conf.Tags["role"] = "kappa"
	conf.Tags["cluster"] = s.config.ClusterName
	conf.Tags["build"] = s.config.Build
	conf.Tags["port"] = fmt.Sprintf("%d", port)
	if s.config.Bootstrap {
		conf.Tags["bootstrap"] = "1"
	}
	if s.config.BootstrapExpect != 0 {
		conf.Tags["expect"] = fmt.Sprintf("%d", s.config.BootstrapExpect)
	}

	conf.MemberlistConfig.LogOutput = s.config.LogOutput
	conf.LogOutput = s.config.LogOutput
	conf.EventCh = s.serfEventCh
	conf.SnapshotPath = filepath.Join(s.config.DataPath, serfSnapshot)
	conf.ProtocolVersion = conf.ProtocolVersion
	conf.RejoinAfterLeave = true
	conf.EnableNameConflictResolution = false

	conf.Merge = &mergeDelegate{name: s.config.ClusterName}
	if err := ensurePath(conf.SnapshotPath, false); err != nil {
		return nil, err
	}
	return serf.Create(conf)
}

// Join is used to have Kappa join the cluster.
// The target address should be another node inside the cluster
// listening on the Serf address
func (s *Server) Join(addrs []string) (int, error) {
	return s.serf.Join(addrs, true)
}

// LocalMember is used to return the local node
func (c *Server) LocalMember() serf.Member {
	return c.serf.LocalMember()
}

// Members is used to return the members of the cluster
func (s *Server) Members() []serf.Member {
	return s.serf.Members()
}

// RemoveFailedNode is used to remove a failed node from the cluster
func (s *Server) RemoveFailedNode(node string) error {
	if err := s.serf.RemoveFailedNode(node); err != nil {
		return err
	}
	return nil
}

// IsLeader checks if this server is the cluster leader
func (s *Server) IsLeader() bool {
	return true
	// return s.raft.State() == raft.Leader
}

// KeyManager returns the Serf keyring manager
func (s *Server) KeyManager() *serf.KeyManager {
	return s.serf.KeyManager()
}

// Encrypted determines if gossip is encrypted
func (s *Server) Encrypted() bool {
	return s.serf.EncryptionEnabled()
}
<|endoftext|>


<commit_before>/*
The MIT License (MIT)

Copyright (c) 2014-2017 DutchCoders [https://github.com/dutchcoders/]

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/

package server

import (
	"errors"
	gorillaHandlers "github.com/gorilla/handlers"
	"log"
	"math/rand"
	"mime"
	"net/http"
	"net/url"
	"os"
	"os/signal"
	"strings"
	"sync"
	"syscall"
	"time"

	context "golang.org/x/net/context"

	"github.com/PuerkitoBio/ghost/handlers"
	"github.com/VojtechVitek/ratelimit"
	"github.com/VojtechVitek/ratelimit/memory"
	"github.com/gorilla/mux"

	_ "net/http/pprof"

	"crypto/tls"

	web "github.com/dutchcoders/transfer.sh-web"
	assetfs "github.com/elazarl/go-bindata-assetfs"

	autocert "golang.org/x/crypto/acme/autocert"
	"path/filepath"
)

const SERVER_INFO = "transfer.sh"

// parse request with maximum memory of _24Kilobits
const _24K = (1 << 3) * 24

// parse request with maximum memory of _5Megabytes
const _5M = (1 << 20) * 5

type OptionFn func(*Server)

func ClamavHost(s string) OptionFn {
	return func(srvr *Server) {
		srvr.ClamAVDaemonHost = s
	}
}

func VirustotalKey(s string) OptionFn {
	return func(srvr *Server) {
		srvr.VirusTotalKey = s
	}
}

func Listener(s string) OptionFn {
	return func(srvr *Server) {
		srvr.ListenerString = s
	}

}

func CorsDomains(s string) OptionFn {
	return func(srvr *Server) {
		srvr.CorsDomains = s
	}

}

func GoogleAnalytics(gaKey string) OptionFn {
	return func(srvr *Server) {
		srvr.gaKey = gaKey
	}
}

func UserVoice(userVoiceKey string) OptionFn {
	return func(srvr *Server) {
		srvr.userVoiceKey = userVoiceKey
	}
}

func TLSListener(s string, t bool) OptionFn {
	return func(srvr *Server) {
		srvr.TLSListenerString = s
		srvr.TLSListenerOnly = t
	}

}

func ProfileListener(s string) OptionFn {
	return func(srvr *Server) {
		srvr.ProfileListenerString = s
	}
}

func WebPath(s string) OptionFn {
	return func(srvr *Server) {
		if s[len(s)-1:] != "/" {
			s = s + string(filepath.Separator)
		}

		srvr.webPath = s
	}
}

func ProxyPath(s string) OptionFn {
	return func(srvr *Server) {
		if s[len(s)-1:] != "/" {
			s = s + string(filepath.Separator)
		}

		srvr.proxyPath = s
	}
}

func TempPath(s string) OptionFn {
	return func(srvr *Server) {
		if s[len(s)-1:] != "/" {
			s = s + string(filepath.Separator)
		}

		srvr.tempPath = s
	}
}

func LogFile(logger *log.Logger, s string) OptionFn {
	return func(srvr *Server) {
		f, err := os.OpenFile(s, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
		if err != nil {
			log.Fatalf("error opening file: %v", err)
		}

		logger.SetOutput(f)
		srvr.logger = logger
	}
}

func Logger(logger *log.Logger) OptionFn {
	return func(srvr *Server) {
		srvr.logger = logger
	}
}

func RateLimit(requests int) OptionFn {
	return func(srvr *Server) {
		srvr.rateLimitRequests = requests
	}
}

func ForceHTTPs() OptionFn {
	return func(srvr *Server) {
		srvr.forceHTTPs = true
	}
}

func EnableProfiler() OptionFn {
	return func(srvr *Server) {
		srvr.profilerEnabled = true
	}
}

func UseStorage(s Storage) OptionFn {
	return func(srvr *Server) {
		srvr.storage = s
	}
}

func UseLetsEncrypt(hosts []string) OptionFn {
	return func(srvr *Server) {
		cacheDir := "./cache/"

		m := autocert.Manager{
			Prompt: autocert.AcceptTOS,
			Cache:  autocert.DirCache(cacheDir),
			HostPolicy: func(_ context.Context, host string) error {
				found := false

				for _, h := range hosts {
					found = found || strings.HasSuffix(host, h)
				}

				if !found {
					return errors.New("acme/autocert: host not configured")
				}

				return nil
			},
		}

		srvr.tlsConfig = &tls.Config{
			GetCertificate: m.GetCertificate,
		}
	}
}

func TLSConfig(cert, pk string) OptionFn {
	certificate, err := tls.LoadX509KeyPair(cert, pk)
	return func(srvr *Server) {
		srvr.tlsConfig = &tls.Config{
			GetCertificate: func(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) {
				return &certificate, err
			},
		}
	}
}

func HttpAuthCredentials(user string, pass string) OptionFn {
	return func(srvr *Server) {
		srvr.AuthUser = user
		srvr.AuthPass = pass
	}
}

func FilterOptions(options IPFilterOptions) OptionFn {
	for i, allowedIP := range options.AllowedIPs {
		options.AllowedIPs[i] = strings.TrimSpace(allowedIP)
	}

	for i, blockedIP := range options.BlockedIPs {
		options.BlockedIPs[i] = strings.TrimSpace(blockedIP)
	}

	return func(srvr *Server) {
		srvr.ipFilterOptions = &options
	}
}

type Server struct {
	AuthUser string
	AuthPass string

	logger *log.Logger

	tlsConfig *tls.Config

	profilerEnabled bool

	locks map[string]*sync.Mutex

	rateLimitRequests int

	storage Storage

	forceHTTPs bool

	ipFilterOptions *IPFilterOptions

	VirusTotalKey    string
	ClamAVDaemonHost string

	tempPath string

	webPath      string
	proxyPath    string
	gaKey        string
	userVoiceKey string

	TLSListenerOnly bool

	CorsDomains           string
	ListenerString        string
	TLSListenerString     string
	ProfileListenerString string

	Certificate string

	LetsEncryptCache string
}

func New(options ...OptionFn) (*Server, error) {
	s := &Server{
		locks: map[string]*sync.Mutex{},
	}

	for _, optionFn := range options {
		optionFn(s)
	}

	return s, nil
}

func init() {
	rand.Seed(time.Now().UTC().UnixNano())
}

func (s *Server) Run() {
	listening := false

	if s.profilerEnabled {
		listening = true

		go func() {
			s.logger.Println("Profiled listening at: :6060")

			http.ListenAndServe(":6060", nil)
		}()
	}

	r := mux.NewRouter()

	var fs http.FileSystem

	if s.webPath != "" {
		s.logger.Println("Using static file path: ", s.webPath)

		fs = http.Dir(s.webPath)

		htmlTemplates, _ = htmlTemplates.ParseGlob(s.webPath + "*.html")
		textTemplates, _ = textTemplates.ParseGlob(s.webPath + "*.txt")
	} else {
		fs = &assetfs.AssetFS{
			Asset:    web.Asset,
			AssetDir: web.AssetDir,
			AssetInfo: func(path string) (os.FileInfo, error) {
				return os.Stat(path)
			},
			Prefix: web.Prefix,
		}

		for _, path := range web.AssetNames() {
			bytes, err := web.Asset(path)
			if err != nil {
				s.logger.Panicf("Unable to parse: path=%s, err=%s", path, err)
			}

			htmlTemplates.New(stripPrefix(path)).Parse(string(bytes))
			textTemplates.New(stripPrefix(path)).Parse(string(bytes))
		}
	}

	staticHandler := http.FileServer(fs)

	r.PathPrefix("/images/").Handler(staticHandler).Methods("GET")
	r.PathPrefix("/styles/").Handler(staticHandler).Methods("GET")
	r.PathPrefix("/scripts/").Handler(staticHandler).Methods("GET")
	r.PathPrefix("/fonts/").Handler(staticHandler).Methods("GET")
	r.PathPrefix("/ico/").Handler(staticHandler).Methods("GET")
	r.HandleFunc("/favicon.ico", staticHandler.ServeHTTP).Methods("GET")
	r.HandleFunc("/robots.txt", staticHandler.ServeHTTP).Methods("GET")

	r.HandleFunc("/{filename:(?:favicon\\.ico|robots\\.txt|health\\.html)}", s.BasicAuthHandler(http.HandlerFunc(s.putHandler))).Methods("PUT")

	r.HandleFunc("/health.html", healthHandler).Methods("GET")
	r.HandleFunc("/", s.viewHandler).Methods("GET")

	r.HandleFunc("/({files:.*}).zip", s.zipHandler).Methods("GET")
	r.HandleFunc("/({files:.*}).tar", s.tarHandler).Methods("GET")
	r.HandleFunc("/({files:.*}).tar.gz", s.tarGzHandler).Methods("GET")

	r.HandleFunc("/{token}/{filename}", s.headHandler).Methods("HEAD")
	r.HandleFunc("/{action:(?:download|get|inline)}/{token}/{filename}", s.headHandler).Methods("HEAD")

	r.HandleFunc("/{token}/{filename}", s.previewHandler).MatcherFunc(func(r *http.Request, rm *mux.RouteMatch) (match bool) {
		match = false

		// The file will show a preview page when opening the link in browser directly or
		// from external link. If the referer url path and current path are the same it will be
		// downloaded.
		if !acceptsHTML(r.Header) {
			return false
		}

		match = (r.Referer() == "")

		u, err := url.Parse(r.Referer())
		if err != nil {
			s.logger.Fatal(err)
			return
		}

		match = match || (u.Path != r.URL.Path)
		return
	}).Methods("GET")

	getHandlerFn := s.getHandler
	if s.rateLimitRequests > 0 {
		getHandlerFn = ratelimit.Request(ratelimit.IP).Rate(s.rateLimitRequests, 60*time.Second).LimitBy(memory.New())(http.HandlerFunc(getHandlerFn)).ServeHTTP
	}

	r.HandleFunc("/{token}/{filename}", getHandlerFn).Methods("GET")
	r.HandleFunc("/{action:(?:download|get|inline)}/{token}/{filename}", getHandlerFn).Methods("GET")

	r.HandleFunc("/{filename}/virustotal", s.virusTotalHandler).Methods("PUT")
	r.HandleFunc("/{filename}/scan", s.scanHandler).Methods("PUT")
	r.HandleFunc("/put/{filename}", s.BasicAuthHandler(http.HandlerFunc(s.putHandler))).Methods("PUT")
	r.HandleFunc("/upload/{filename}", s.BasicAuthHandler(http.HandlerFunc(s.putHandler))).Methods("PUT")
	r.HandleFunc("/{filename}", s.BasicAuthHandler(http.HandlerFunc(s.putHandler))).Methods("PUT")
	r.HandleFunc("/", s.BasicAuthHandler(http.HandlerFunc(s.postHandler))).Methods("POST")
	// r.HandleFunc("/{page}", viewHandler).Methods("GET")

	r.HandleFunc("/{token}/{filename}/{deletionToken}", s.deleteHandler).Methods("DELETE")

	r.NotFoundHandler = http.HandlerFunc(s.notFoundHandler)

	mime.AddExtensionType(".md", "text/x-markdown")

	s.logger.Printf("Transfer.sh server started.\nusing temp folder: %s\nusing storage provider: %s", s.tempPath, s.storage.Type())

	var cors func(http.Handler) http.Handler
	if len(s.CorsDomains) > 0 {
		cors = gorillaHandlers.CORS(
			gorillaHandlers.AllowedHeaders([]string{"*"}),
			gorillaHandlers.AllowedOrigins(strings.Split(s.CorsDomains, ",")),
			gorillaHandlers.AllowedMethods([]string{"GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS"}),
		)
	} else {
		cors = func(h http.Handler) http.Handler {
			return h
		}
	}

	h := handlers.PanicHandler(
		IPFilterHandler(
			handlers.LogHandler(
				LoveHandler(
					s.RedirectHandler(cors(r))),
				handlers.NewLogOptions(s.logger.Printf, "_default_"),
			),
			s.ipFilterOptions,
		),
		nil,
	)

	if !s.TLSListenerOnly {
		srvr := &http.Server{
			Addr:    s.ListenerString,
			Handler: h,
		}

		listening = true
		s.logger.Printf("listening on port: %v\n", s.ListenerString)

		go func() {
			srvr.ListenAndServe()
		}()
	}

	if s.TLSListenerString != "" {
		listening = true
		s.logger.Printf("listening on port: %v\n", s.TLSListenerString)

		go func() {
			s := &http.Server{
				Addr:      s.TLSListenerString,
				Handler:   h,
				TLSConfig: s.tlsConfig,
			}

			if err := s.ListenAndServeTLS("", ""); err != nil {
				panic(err)
			}
		}()
	}

	s.logger.Printf("---------------------------")

	term := make(chan os.Signal, 1)
	signal.Notify(term, os.Interrupt)
	signal.Notify(term, syscall.SIGTERM)

	if listening {
		<-term
	} else {
		s.logger.Printf("No listener active.")
	}

	s.logger.Printf("Server stopped.")
}
<commit_msg>use cryptographically secure rng seed<commit_after>/*
The MIT License (MIT)

Copyright (c) 2014-2017 DutchCoders [https://github.com/dutchcoders/]

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/

package server

import (
	"errors"
	gorillaHandlers "github.com/gorilla/handlers"
	"log"
	crypto_rand "crypto/rand"
	"encoding/binary"
	"math/rand"
	"mime"
	"net/http"
	"net/url"
	"os"
	"os/signal"
	"strings"
	"sync"
	"syscall"
	"time"

	context "golang.org/x/net/context"

	"github.com/PuerkitoBio/ghost/handlers"
	"github.com/VojtechVitek/ratelimit"
	"github.com/VojtechVitek/ratelimit/memory"
	"github.com/gorilla/mux"

	_ "net/http/pprof"

	"crypto/tls"

	web "github.com/dutchcoders/transfer.sh-web"
	assetfs "github.com/elazarl/go-bindata-assetfs"

	autocert "golang.org/x/crypto/acme/autocert"
	"path/filepath"
)

const SERVER_INFO = "transfer.sh"

// parse request with maximum memory of _24Kilobits
const _24K = (1 << 3) * 24

// parse request with maximum memory of _5Megabytes
const _5M = (1 << 20) * 5

type OptionFn func(*Server)

func ClamavHost(s string) OptionFn {
	return func(srvr *Server) {
		srvr.ClamAVDaemonHost = s
	}
}

func VirustotalKey(s string) OptionFn {
	return func(srvr *Server) {
		srvr.VirusTotalKey = s
	}
}

func Listener(s string) OptionFn {
	return func(srvr *Server) {
		srvr.ListenerString = s
	}

}

func CorsDomains(s string) OptionFn {
	return func(srvr *Server) {
		srvr.CorsDomains = s
	}

}

func GoogleAnalytics(gaKey string) OptionFn {
	return func(srvr *Server) {
		srvr.gaKey = gaKey
	}
}

func UserVoice(userVoiceKey string) OptionFn {
	return func(srvr *Server) {
		srvr.userVoiceKey = userVoiceKey
	}
}

func TLSListener(s string, t bool) OptionFn {
	return func(srvr *Server) {
		srvr.TLSListenerString = s
		srvr.TLSListenerOnly = t
	}

}

func ProfileListener(s string) OptionFn {
	return func(srvr *Server) {
		srvr.ProfileListenerString = s
	}
}

func WebPath(s string) OptionFn {
	return func(srvr *Server) {
		if s[len(s)-1:] != "/" {
			s = s + string(filepath.Separator)
		}

		srvr.webPath = s
	}
}

func ProxyPath(s string) OptionFn {
	return func(srvr *Server) {
		if s[len(s)-1:] != "/" {
			s = s + string(filepath.Separator)
		}

		srvr.proxyPath = s
	}
}

func TempPath(s string) OptionFn {
	return func(srvr *Server) {
		if s[len(s)-1:] != "/" {
			s = s + string(filepath.Separator)
		}

		srvr.tempPath = s
	}
}

func LogFile(logger *log.Logger, s string) OptionFn {
	return func(srvr *Server) {
		f, err := os.OpenFile(s, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
		if err != nil {
			log.Fatalf("error opening file: %v", err)
		}

		logger.SetOutput(f)
		srvr.logger = logger
	}
}

func Logger(logger *log.Logger) OptionFn {
	return func(srvr *Server) {
		srvr.logger = logger
	}
}

func RateLimit(requests int) OptionFn {
	return func(srvr *Server) {
		srvr.rateLimitRequests = requests
	}
}

func ForceHTTPs() OptionFn {
	return func(srvr *Server) {
		srvr.forceHTTPs = true
	}
}

func EnableProfiler() OptionFn {
	return func(srvr *Server) {
		srvr.profilerEnabled = true
	}
}

func UseStorage(s Storage) OptionFn {
	return func(srvr *Server) {
		srvr.storage = s
	}
}

func UseLetsEncrypt(hosts []string) OptionFn {
	return func(srvr *Server) {
		cacheDir := "./cache/"

		m := autocert.Manager{
			Prompt: autocert.AcceptTOS,
			Cache:  autocert.DirCache(cacheDir),
			HostPolicy: func(_ context.Context, host string) error {
				found := false

				for _, h := range hosts {
					found = found || strings.HasSuffix(host, h)
				}

				if !found {
					return errors.New("acme/autocert: host not configured")
				}

				return nil
			},
		}

		srvr.tlsConfig = &tls.Config{
			GetCertificate: m.GetCertificate,
		}
	}
}

func TLSConfig(cert, pk string) OptionFn {
	certificate, err := tls.LoadX509KeyPair(cert, pk)
	return func(srvr *Server) {
		srvr.tlsConfig = &tls.Config{
			GetCertificate: func(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) {
				return &certificate, err
			},
		}
	}
}

func HttpAuthCredentials(user string, pass string) OptionFn {
	return func(srvr *Server) {
		srvr.AuthUser = user
		srvr.AuthPass = pass
	}
}

func FilterOptions(options IPFilterOptions) OptionFn {
	for i, allowedIP := range options.AllowedIPs {
		options.AllowedIPs[i] = strings.TrimSpace(allowedIP)
	}

	for i, blockedIP := range options.BlockedIPs {
		options.BlockedIPs[i] = strings.TrimSpace(blockedIP)
	}

	return func(srvr *Server) {
		srvr.ipFilterOptions = &options
	}
}

type Server struct {
	AuthUser string
	AuthPass string

	logger *log.Logger

	tlsConfig *tls.Config

	profilerEnabled bool

	locks map[string]*sync.Mutex

	rateLimitRequests int

	storage Storage

	forceHTTPs bool

	ipFilterOptions *IPFilterOptions

	VirusTotalKey    string
	ClamAVDaemonHost string

	tempPath string

	webPath      string
	proxyPath    string
	gaKey        string
	userVoiceKey string

	TLSListenerOnly bool

	CorsDomains           string
	ListenerString        string
	TLSListenerString     string
	ProfileListenerString string

	Certificate string

	LetsEncryptCache string
}

func New(options ...OptionFn) (*Server, error) {
	s := &Server{
		locks: map[string]*sync.Mutex{},
	}

	for _, optionFn := range options {
		optionFn(s)
	}

	return s, nil
}

func init() {
	var seedBytes [8]byte
	if _, err := crypto_rand.Read(seedBytes[:]); err != nil {
		panic("cannot obtain cryptographically secure seed")
	}
	rand.Seed(int64(binary.LittleEndian.Uint64(seedBytes[:])))
}

func (s *Server) Run() {
	listening := false

	if s.profilerEnabled {
		listening = true

		go func() {
			s.logger.Println("Profiled listening at: :6060")

			http.ListenAndServe(":6060", nil)
		}()
	}

	r := mux.NewRouter()

	var fs http.FileSystem

	if s.webPath != "" {
		s.logger.Println("Using static file path: ", s.webPath)

		fs = http.Dir(s.webPath)

		htmlTemplates, _ = htmlTemplates.ParseGlob(s.webPath + "*.html")
		textTemplates, _ = textTemplates.ParseGlob(s.webPath + "*.txt")
	} else {
		fs = &assetfs.AssetFS{
			Asset:    web.Asset,
			AssetDir: web.AssetDir,
			AssetInfo: func(path string) (os.FileInfo, error) {
				return os.Stat(path)
			},
			Prefix: web.Prefix,
		}

		for _, path := range web.AssetNames() {
			bytes, err := web.Asset(path)
			if err != nil {
				s.logger.Panicf("Unable to parse: path=%s, err=%s", path, err)
			}

			htmlTemplates.New(stripPrefix(path)).Parse(string(bytes))
			textTemplates.New(stripPrefix(path)).Parse(string(bytes))
		}
	}

	staticHandler := http.FileServer(fs)

	r.PathPrefix("/images/").Handler(staticHandler).Methods("GET")
	r.PathPrefix("/styles/").Handler(staticHandler).Methods("GET")
	r.PathPrefix("/scripts/").Handler(staticHandler).Methods("GET")
	r.PathPrefix("/fonts/").Handler(staticHandler).Methods("GET")
	r.PathPrefix("/ico/").Handler(staticHandler).Methods("GET")
	r.HandleFunc("/favicon.ico", staticHandler.ServeHTTP).Methods("GET")
	r.HandleFunc("/robots.txt", staticHandler.ServeHTTP).Methods("GET")

	r.HandleFunc("/{filename:(?:favicon\\.ico|robots\\.txt|health\\.html)}", s.BasicAuthHandler(http.HandlerFunc(s.putHandler))).Methods("PUT")

	r.HandleFunc("/health.html", healthHandler).Methods("GET")
	r.HandleFunc("/", s.viewHandler).Methods("GET")

	r.HandleFunc("/({files:.*}).zip", s.zipHandler).Methods("GET")
	r.HandleFunc("/({files:.*}).tar", s.tarHandler).Methods("GET")
	r.HandleFunc("/({files:.*}).tar.gz", s.tarGzHandler).Methods("GET")

	r.HandleFunc("/{token}/{filename}", s.headHandler).Methods("HEAD")
	r.HandleFunc("/{action:(?:download|get|inline)}/{token}/{filename}", s.headHandler).Methods("HEAD")

	r.HandleFunc("/{token}/{filename}", s.previewHandler).MatcherFunc(func(r *http.Request, rm *mux.RouteMatch) (match bool) {
		match = false

		// The file will show a preview page when opening the link in browser directly or
		// from external link. If the referer url path and current path are the same it will be
		// downloaded.
		if !acceptsHTML(r.Header) {
			return false
		}

		match = (r.Referer() == "")

		u, err := url.Parse(r.Referer())
		if err != nil {
			s.logger.Fatal(err)
			return
		}

		match = match || (u.Path != r.URL.Path)
		return
	}).Methods("GET")

	getHandlerFn := s.getHandler
	if s.rateLimitRequests > 0 {
		getHandlerFn = ratelimit.Request(ratelimit.IP).Rate(s.rateLimitRequests, 60*time.Second).LimitBy(memory.New())(http.HandlerFunc(getHandlerFn)).ServeHTTP
	}

	r.HandleFunc("/{token}/{filename}", getHandlerFn).Methods("GET")
	r.HandleFunc("/{action:(?:download|get|inline)}/{token}/{filename}", getHandlerFn).Methods("GET")

	r.HandleFunc("/{filename}/virustotal", s.virusTotalHandler).Methods("PUT")
	r.HandleFunc("/{filename}/scan", s.scanHandler).Methods("PUT")
	r.HandleFunc("/put/{filename}", s.BasicAuthHandler(http.HandlerFunc(s.putHandler))).Methods("PUT")
	r.HandleFunc("/upload/{filename}", s.BasicAuthHandler(http.HandlerFunc(s.putHandler))).Methods("PUT")
	r.HandleFunc("/{filename}", s.BasicAuthHandler(http.HandlerFunc(s.putHandler))).Methods("PUT")
	r.HandleFunc("/", s.BasicAuthHandler(http.HandlerFunc(s.postHandler))).Methods("POST")
	// r.HandleFunc("/{page}", viewHandler).Methods("GET")

	r.HandleFunc("/{token}/{filename}/{deletionToken}", s.deleteHandler).Methods("DELETE")

	r.NotFoundHandler = http.HandlerFunc(s.notFoundHandler)

	mime.AddExtensionType(".md", "text/x-markdown")

	s.logger.Printf("Transfer.sh server started.\nusing temp folder: %s\nusing storage provider: %s", s.tempPath, s.storage.Type())

	var cors func(http.Handler) http.Handler
	if len(s.CorsDomains) > 0 {
		cors = gorillaHandlers.CORS(
			gorillaHandlers.AllowedHeaders([]string{"*"}),
			gorillaHandlers.AllowedOrigins(strings.Split(s.CorsDomains, ",")),
			gorillaHandlers.AllowedMethods([]string{"GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS"}),
		)
	} else {
		cors = func(h http.Handler) http.Handler {
			return h
		}
	}

	h := handlers.PanicHandler(
		IPFilterHandler(
			handlers.LogHandler(
				LoveHandler(
					s.RedirectHandler(cors(r))),
				handlers.NewLogOptions(s.logger.Printf, "_default_"),
			),
			s.ipFilterOptions,
		),
		nil,
	)

	if !s.TLSListenerOnly {
		srvr := &http.Server{
			Addr:    s.ListenerString,
			Handler: h,
		}

		listening = true
		s.logger.Printf("listening on port: %v\n", s.ListenerString)

		go func() {
			srvr.ListenAndServe()
		}()
	}

	if s.TLSListenerString != "" {
		listening = true
		s.logger.Printf("listening on port: %v\n", s.TLSListenerString)

		go func() {
			s := &http.Server{
				Addr:      s.TLSListenerString,
				Handler:   h,
				TLSConfig: s.tlsConfig,
			}

			if err := s.ListenAndServeTLS("", ""); err != nil {
				panic(err)
			}
		}()
	}

	s.logger.Printf("---------------------------")

	term := make(chan os.Signal, 1)
	signal.Notify(term, os.Interrupt)
	signal.Notify(term, syscall.SIGTERM)

	if listening {
		<-term
	} else {
		s.logger.Printf("No listener active.")
	}

	s.logger.Printf("Server stopped.")
}
<|endoftext|>


<commit_before>package server

import (
	"fmt"
	"io/ioutil"
	"net/http"

	"github.com/Sirupsen/logrus"
	"github.com/bitly/go-nsq"
)

const ROUTE = "/{user:.*}/{name:.*}/"

// New returns a new http.Handler that handles github webhooks from the github API.
// After receiving a hook the handler will push the message onto the specified NSQ Queue.
//
// producer is the connection to the NSQD instance.
// secret is the secret provided when you register the webhook in the github UI.
// logger is the standard logger for the application
func New(producer *nsq.Producer, secret string, logger *logrus.Logger) http.Handler {
	return &Server{
		producer: producer,
		secret:   secret,
		logger:   logger,
	}
}

// Server handles github webhooks and pushes the messages onto a specified
// queue under the repositories.  The queue name will the be repository name
// prepended with hoosk-{reponame}
type Server struct {
	producer *nsq.Producer
	secret   string
	logger   *logrus.Logger
}

func (h *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	var (
		repo       = parseRepo(r)
		requestLog = h.logger.WithFields(newFields(r, repo))
	)
	requestLog.Debug("web hook received")

	data, err := ioutil.ReadAll(r.Body)
	r.Body.Close()
	if err != nil {
		requestLog.WithField("error", err).Error("read request body")
		http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
		return
	}

	if !validateSignature(requestLog, r, h.secret, data) {
		requestLog.Warn("signature verification failed")
		// return a generic NOTFOUND for auth/verification errors
		http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
		return
	}
	if err := h.producer.Publish(fmt.Sprintf("hooks-%s", repo.Name), data); err != nil {
		requestLog.WithField("error", err).Error("publish payload onto queue")
		http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
		return
	}
}
<commit_msg>Inject Github header in the payload<commit_after>package server

import (
	"fmt"
	"io/ioutil"
	"net/http"

	"github.com/Sirupsen/logrus"
	"github.com/bitly/go-nsq"
	"github.com/bitly/go-simplejson"
)

const ROUTE = "/{user:.*}/{name:.*}/"

// New returns a new http.Handler that handles github webhooks from the github API.
// After receiving a hook the handler will push the message onto the specified NSQ Queue.
//
// producer is the connection to the NSQD instance.
// secret is the secret provided when you register the webhook in the github UI.
// logger is the standard logger for the application
func New(producer *nsq.Producer, secret string, logger *logrus.Logger) http.Handler {
	return &Server{
		producer: producer,
		secret:   secret,
		logger:   logger,
	}
}

// Server handles github webhooks and pushes the messages onto a specified
// queue under the repositories.  The queue name will the be repository name
// prepended with hoosk-{reponame}
type Server struct {
	producer *nsq.Producer
	secret   string
	logger   *logrus.Logger
}

func (h *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	var (
		repo       = parseRepo(r)
		requestLog = h.logger.WithFields(newFields(r, repo))
	)
	requestLog.Debug("web hook received")

	data, err := ioutil.ReadAll(r.Body)
	r.Body.Close()
	if err != nil {
		requestLog.WithField("error", err).Error("read request body")
		http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
		return
	}

	if !validateSignature(requestLog, r, h.secret, data) {
		requestLog.Warn("signature verification failed")
		// return a generic NOTFOUND for auth/verification errors
		http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
		return
	}

	// Inject the Github headers to the payload.
	j, err := simplejson.NewJson(data)
	if err != nil {
		requestLog.WithField("error", err).Error("parse github payload")
		http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
		return
	}
	for _, h := range []string{"X-Github-Event", "X-Hub-Signature", "X-Github-Delivery"} {
		j.Set(h, r.Header.Get(h))
	}
	data, err = j.Encode()
	if err != nil {
		requestLog.WithField("error", err).Error("serialize payload")
		http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
		return
	}

	if err := h.producer.Publish(fmt.Sprintf("hooks-%s", repo.Name), data); err != nil {
		requestLog.WithField("error", err).Error("publish payload onto queue")
		http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
		return
	}
}
<|endoftext|>


<commit_before>package server

import (
	"fmt"
	"regexp"
	"time"

	"github.com/CenturyLinkCloud/clc-sdk/api"
	"github.com/CenturyLinkCloud/clc-sdk/status"
)

var (
	ErrInvalidServer = fmt.Errorf("server: server missing required field(s). (Name, CPU, MemoryGB, GroupID, SourceServerID, Type)")
)

func New(client api.HTTP) *Service {
	return &Service{
		client: client,
		config: client.Config(),
	}
}

type Service struct {
	client api.HTTP
	config *api.Config
}

func (s *Service) Get(name string) (*Response, error) {
	url := fmt.Sprintf("%s/servers/%s/%s", s.config.BaseURL, s.config.Alias, name)
	if regexp.MustCompile("^[0-9a-f]{32}$").MatchString(name) {
		url = fmt.Sprintf("%s?uuid=true", url)
	}
	resp := &Response{}
	err := s.client.Get(url, resp)
	return resp, err
}

func (s *Service) Create(server Server) (*status.QueuedResponse, error) {
	if !server.Valid() {
		return nil, ErrInvalidServer
	}

	resp := &status.QueuedResponse{}
	url := fmt.Sprintf("%s/servers/%s", s.config.BaseURL, s.config.Alias)
	err := s.client.Post(url, server, resp)
	return resp, err
}

func (s *Service) Update(name string, updates ...api.Update) (*status.Status, error) {
	resp := &status.Status{}
	url := fmt.Sprintf("%s/servers/%s/%s", s.config.BaseURL, s.config.Alias, name)
	err := s.client.Patch(url, updates, resp)
	return resp, err
}

func (s *Service) Edit(name string, updates ...api.Update) error {
	url := fmt.Sprintf("%s/servers/%s/%s", s.config.BaseURL, s.config.Alias, name)
	err := s.client.Patch(url, updates, nil)
	return err
}

func (s *Service) Delete(name string) (*status.QueuedResponse, error) {
	url := fmt.Sprintf("%s/servers/%s/%s", s.config.BaseURL, s.config.Alias, name)
	resp := &status.QueuedResponse{}
	err := s.client.Delete(url, resp)
	return resp, err
}

func (s *Service) GetCredentials(name string) (Credentials, error) {
	url := fmt.Sprintf("%s/servers/%s/%s/credentials", s.config.BaseURL, s.config.Alias, name)
	resp := Credentials{}
	err := s.client.Get(url, &resp)
	return resp, err
}

type Credentials struct {
	Username string `json:"userName"`
	Password string `json:"password"`
}

func (s *Service) Archive(servers ...string) ([]*status.QueuedResponse, error) {
	url := fmt.Sprintf("%s/operations/%s/servers/archive", s.config.BaseURL, s.config.Alias)
	var resp []*status.QueuedResponse
	err := s.client.Post(url, servers, &resp)
	return resp, err
}

func (s *Service) Restore(name, group string) (*status.Status, error) {
	restore := map[string]string{"targetGroupId": group}
	url := fmt.Sprintf("%s/servers/%s/%s/restore", s.config.BaseURL, s.config.Alias, name)
	resp := &status.Status{}
	err := s.client.Post(url, restore, resp)
	return resp, err
}

func (s *Service) CreateSnapshot(expiration int, servers ...string) ([]*status.QueuedResponse, error) {
	snapshot := Snapshot{Expiration: expiration, Servers: servers}
	url := fmt.Sprintf("%s/operations/%s/servers/createSnapshot", s.config.BaseURL, s.config.Alias)
	var resp []*status.QueuedResponse
	err := s.client.Post(url, snapshot, &resp)
	return resp, err
}

func (s *Service) DeleteSnapshot(server, id string) (*status.Status, error) {
	url := fmt.Sprintf("%s/servers/%s/%s/snapshots/%s", s.config.BaseURL, s.config.Alias, server, id)
	resp := &status.Status{}
	err := s.client.Delete(url, resp)
	return resp, err
}

func (s *Service) RevertSnapshot(server, id string) (*status.Status, error) {
	url := fmt.Sprintf("%s/servers/%s/%s/snapshots/%s/restore", s.config.BaseURL, s.config.Alias, server, id)
	resp := &status.Status{}
	err := s.client.Post(url, nil, resp)
	return resp, err
}

type Snapshot struct {
	Expiration int      `json:"snapshotExpirationDays"`
	Servers    []string `json:"serverIds"`
}

func (s *Service) ExecutePackage(pkg Package, servers ...string) ([]*status.QueuedResponse, error) {
	url := fmt.Sprintf("%s/operations/%s/servers/executePackage", s.config.BaseURL, s.config.Alias)
	var resp []*status.QueuedResponse
	exec := executePackage{Servers: servers, Package: pkg}
	err := s.client.Post(url, exec, &resp)
	return resp, err
}

type executePackage struct {
	Servers []string `json:"servers"`
	Package Package  `json:"package"`
}

type Package struct {
	ID     string            `json:"packageId"`
	Params map[string]string `json:"parameters"`
}

func (s *Service) PowerState(state PowerState, servers ...string) ([]*status.QueuedResponse, error) {
	url := fmt.Sprintf("%s/operations/%s/servers/%s", s.config.BaseURL, s.config.Alias, state)
	var resp []*status.QueuedResponse
	err := s.client.Post(url, servers, &resp)
	return resp, err
}

type PowerState int

const (
	On = iota
	Off
	Pause
	Reboot
	Reset
	ShutDown
	StartMaintenance
	StopMaintenance
)

func (p PowerState) String() string {
	switch p {
	case On:
		return "powerOn"
	case Off:
		return "powerOff"
	case Pause:
		return "pause"
	case Reboot:
		return "reboot"
	case Reset:
		return "reset"
	case ShutDown:
		return "shutDown"
	case StartMaintenance:
		return "startMaintenance"
	case StopMaintenance:
		return "stopMaintenance"
	}
	return ""
}

func (s *Service) GetPublicIP(name string, ip string) (*PublicIP, error) {
	url := fmt.Sprintf("%s/servers/%s/%s/publicIPAddresses/%s", s.config.BaseURL, s.config.Alias, name, ip)
	resp := &PublicIP{}
	err := s.client.Get(url, resp)
	return resp, err
}

func (s *Service) AddPublicIP(name string, ip PublicIP) (*status.Status, error) {
	url := fmt.Sprintf("%s/servers/%s/%s/publicIPAddresses", s.config.BaseURL, s.config.Alias, name)
	resp := &status.Status{}
	err := s.client.Post(url, ip, resp)
	return resp, err
}

func (s *Service) UpdatePublicIP(name string, public string, ip PublicIP) (*status.Status, error) {
	url := fmt.Sprintf("%s/servers/%s/%s/publicIPAddresses/%s", s.config.BaseURL, s.config.Alias, name, public)
	resp := &status.Status{}
	err := s.client.Put(url, ip, resp)
	return resp, err
}

func (s *Service) DeletePublicIP(name, ip string) (*status.Status, error) {
	url := fmt.Sprintf("%s/servers/%s/%s/publicIPAddresses/%s", s.config.BaseURL, s.config.Alias, name, ip)
	resp := &status.Status{}
	err := s.client.Delete(url, resp)
	return resp, err
}

type PublicIP struct {
	InternalIP         string              `json:"internalIPAddress,omitempty"`
	Ports              []Port              `json:"ports,omitempty"`
	SourceRestrictions []SourceRestriction `json:"sourceRestrictions,omitempty"`
}

type Port struct {
	Protocol string `json:"protocol"`
	Port     int    `json:"port"`
	PortTo   int    `json:"portTo,omitempty"`
}

type SourceRestriction struct {
	CIDR string `json:"cidr"`
}

type Disk struct {
	DiskID string `json:"diskId,omitempty"`
	Path   string `json:"path,omitempty"`
	SizeGB int    `json:"sizeGB,omitempty"`
	Type   string `json:"type,omitempty"`
}

func (s *Service) AddSecondaryNetwork(name, networkId, ip string) (*status.Status, error) {
	url := fmt.Sprintf("%s/servers/%s/%s/networks", s.config.BaseURL, s.config.Alias, name)
	req := &SecondaryNetwork{
		NetworkID: networkId,
		IPAddress: ip,
	}
	// returned a non-standard status object, repackage into a proper one
	resp := &status.QueuedOperation{}
	err := s.client.Post(url, req, resp)
	if err != nil {
		return nil, err
	}
	return resp.Status(), nil
}

type SecondaryNetwork struct {
	NetworkID string `json:"networkId,omitempty"`
	IPAddress string `json:"ipAddress,omitempty"`
}

func UpdateCPU(num int) api.Update {
	return api.Update{
		Op:     "set",
		Member: "cpu",
		Value:  num,
	}
}

func UpdateMemory(num int) api.Update {
	return api.Update{
		Op:     "set",
		Member: "memory",
		Value:  num,
	}
}

func UpdateCredentials(current, updated string) api.Update {
	return api.Update{
		Op:     "set",
		Member: "password",
		Value: struct {
			Current  string `json:"current"`
			Password string `json:"password"`
		}{
			current,
			updated,
		},
	}
}

func UpdateGroup(group string) api.Update {
	return api.Update{
		Op:     "set",
		Member: "groupId",
		Value:  group,
	}
}

func UpdateDescription(desc string) api.Update {
	return api.Update{
		Op:     "set",
		Member: "description",
		Value:  desc,
	}
}

func UpdateAdditionaldisks(disks []Disk) api.Update {
	return api.Update{
		Op:     "set",
		Member: "disks",
		Value:  disks,
	}
}

func UpdateCustomfields(fields []api.Customfields) api.Update {
	return api.Update{
		Op:     "set",
		Member: "customFields",
		Value:  fields,
	}
}

type Server struct {
	Name                 string             `json:"name"`
	Description          string             `json:"description,omitempty"`
	GroupID              string             `json:"groupId"`
	SourceServerID       string             `json:"sourceServerId"`
	IsManagedOS          bool               `json:"isManagedOS,omitempty"`
	IsManagedBackup      bool               `json:"isManagedBackup,omitempty"`
	PrimaryDNS           string             `json:"primaryDns,omitempty"`
	SecondaryDNS         string             `json:"secondaryDns,omitempty"`
	NetworkID            string             `json:"networkId,omitempty"`
	IPaddress            string             `json:"ipAddress,omitempty"`
	Password             string             `json:"password,omitempty"`
	SourceServerPassword string             `json:"sourceServerPassword,omitempty"`
	CPU                  int                `json:"cpu"`
	CPUAutoscalePolicyID string             `json:"cpuAutoscalePolicyId,omitempty"`
	MemoryGB             int                `json:"memoryGB"`
	Type                 string             `json:"type"`
	Storagetype          string             `json:"storageType,omitempty"`
	AntiAffinityPolicyID string             `json:"antiAffinityPolicyId,omitempty"`
	Customfields         []api.Customfields `json:"customFields,omitempty"`
	Additionaldisks      []Disk             `json:"additionalDisks,omitempty"`
	TTL                  *time.Time         `json:"ttl,omitempty"`
	Packages             []Package          `json:"packages,omitempty"`
	ConfigurationID      string             `json:"configurationId,omitempty"`
	OSType               string             `json:"osType,omitempty"`
}

func (s *Server) Valid() bool {
	return s.Name != "" && s.CPU != 0 && s.MemoryGB != 0 && s.GroupID != "" && s.SourceServerID != "" && s.Type != ""
}

type Response struct {
	ID          string `json:"id"`
	Name        string `json:"name"`
	Description string `json:"description"`
	GroupID     string `json:"groupId"`
	IsTemplate  bool   `json:"isTemplate"`
	LocationID  string `json:"locationId"`
	OStype      string `json:"osType"`
	Status      string `json:"status"`
	Details     struct {
		IPaddresses []struct {
			Internal string `json:"internal"`
			Public   string `json:"public"`
		} `json:"ipAddresses"`
		AlertPolicies []struct {
			ID    string    `json:"id"`
			Name  string    `json:"name"`
			Links api.Links `json:"links"`
		} `json:"alertPolicies"`
		CPU               int    `json:"cpu"`
		Diskcount         int    `json:"diskCount"`
		Hostname          string `json:"hostName"`
		InMaintenanceMode bool   `json:"inMaintenanceMode"`
		MemoryMB          int    `json:"memoryMB"`
		Powerstate        string `json:"powerState"`
		Storagegb         int    `json:"storageGB"`
		Disks             []struct {
			ID             string        `json:"id"`
			SizeGB         int           `json:"sizeGB"`
			PartitionPaths []interface{} `json:"partitionPaths"`
		} `json:"disks"`
		Partitions []struct {
			SizeGB float64 `json:"sizeGB"`
			Path   string  `json:"path"`
		} `json:"partitions"`
		Snapshots []struct {
			Name  string    `json:"name"`
			Links api.Links `json:"links"`
		} `json:"snapshots"`
		Customfields []api.Customfields `json:"customFields,omitempty"`
	} `json:"details"`
	Type        string `json:"type"`
	Storagetype string `json:"storageType"`
	ChangeInfo  struct {
		CreatedDate  string `json:"createdDate"`
		CreatedBy    string `json:"createdBy"`
		ModifiedDate string `json:"modifiedDate"`
		ModifiedBy   string `json:"modifiedBy"`
	} `json:"changeInfo"`
	Links api.Links `json:"links"`
}
<commit_msg>include OS field (though undocumented)<commit_after>package server

import (
	"fmt"
	"regexp"
	"time"

	"github.com/CenturyLinkCloud/clc-sdk/api"
	"github.com/CenturyLinkCloud/clc-sdk/status"
)

var (
	ErrInvalidServer = fmt.Errorf("server: server missing required field(s). (Name, CPU, MemoryGB, GroupID, SourceServerID, Type)")
)

func New(client api.HTTP) *Service {
	return &Service{
		client: client,
		config: client.Config(),
	}
}

type Service struct {
	client api.HTTP
	config *api.Config
}

func (s *Service) Get(name string) (*Response, error) {
	url := fmt.Sprintf("%s/servers/%s/%s", s.config.BaseURL, s.config.Alias, name)
	if regexp.MustCompile("^[0-9a-f]{32}$").MatchString(name) {
		url = fmt.Sprintf("%s?uuid=true", url)
	}
	resp := &Response{}
	err := s.client.Get(url, resp)
	return resp, err
}

func (s *Service) Create(server Server) (*status.QueuedResponse, error) {
	if !server.Valid() {
		return nil, ErrInvalidServer
	}

	resp := &status.QueuedResponse{}
	url := fmt.Sprintf("%s/servers/%s", s.config.BaseURL, s.config.Alias)
	err := s.client.Post(url, server, resp)
	return resp, err
}

func (s *Service) Update(name string, updates ...api.Update) (*status.Status, error) {
	resp := &status.Status{}
	url := fmt.Sprintf("%s/servers/%s/%s", s.config.BaseURL, s.config.Alias, name)
	err := s.client.Patch(url, updates, resp)
	return resp, err
}

func (s *Service) Edit(name string, updates ...api.Update) error {
	url := fmt.Sprintf("%s/servers/%s/%s", s.config.BaseURL, s.config.Alias, name)
	err := s.client.Patch(url, updates, nil)
	return err
}

func (s *Service) Delete(name string) (*status.QueuedResponse, error) {
	url := fmt.Sprintf("%s/servers/%s/%s", s.config.BaseURL, s.config.Alias, name)
	resp := &status.QueuedResponse{}
	err := s.client.Delete(url, resp)
	return resp, err
}

func (s *Service) GetCredentials(name string) (Credentials, error) {
	url := fmt.Sprintf("%s/servers/%s/%s/credentials", s.config.BaseURL, s.config.Alias, name)
	resp := Credentials{}
	err := s.client.Get(url, &resp)
	return resp, err
}

type Credentials struct {
	Username string `json:"userName"`
	Password string `json:"password"`
}

func (s *Service) Archive(servers ...string) ([]*status.QueuedResponse, error) {
	url := fmt.Sprintf("%s/operations/%s/servers/archive", s.config.BaseURL, s.config.Alias)
	var resp []*status.QueuedResponse
	err := s.client.Post(url, servers, &resp)
	return resp, err
}

func (s *Service) Restore(name, group string) (*status.Status, error) {
	restore := map[string]string{"targetGroupId": group}
	url := fmt.Sprintf("%s/servers/%s/%s/restore", s.config.BaseURL, s.config.Alias, name)
	resp := &status.Status{}
	err := s.client.Post(url, restore, resp)
	return resp, err
}

func (s *Service) CreateSnapshot(expiration int, servers ...string) ([]*status.QueuedResponse, error) {
	snapshot := Snapshot{Expiration: expiration, Servers: servers}
	url := fmt.Sprintf("%s/operations/%s/servers/createSnapshot", s.config.BaseURL, s.config.Alias)
	var resp []*status.QueuedResponse
	err := s.client.Post(url, snapshot, &resp)
	return resp, err
}

func (s *Service) DeleteSnapshot(server, id string) (*status.Status, error) {
	url := fmt.Sprintf("%s/servers/%s/%s/snapshots/%s", s.config.BaseURL, s.config.Alias, server, id)
	resp := &status.Status{}
	err := s.client.Delete(url, resp)
	return resp, err
}

func (s *Service) RevertSnapshot(server, id string) (*status.Status, error) {
	url := fmt.Sprintf("%s/servers/%s/%s/snapshots/%s/restore", s.config.BaseURL, s.config.Alias, server, id)
	resp := &status.Status{}
	err := s.client.Post(url, nil, resp)
	return resp, err
}

type Snapshot struct {
	Expiration int      `json:"snapshotExpirationDays"`
	Servers    []string `json:"serverIds"`
}

func (s *Service) ExecutePackage(pkg Package, servers ...string) ([]*status.QueuedResponse, error) {
	url := fmt.Sprintf("%s/operations/%s/servers/executePackage", s.config.BaseURL, s.config.Alias)
	var resp []*status.QueuedResponse
	exec := executePackage{Servers: servers, Package: pkg}
	err := s.client.Post(url, exec, &resp)
	return resp, err
}

type executePackage struct {
	Servers []string `json:"servers"`
	Package Package  `json:"package"`
}

type Package struct {
	ID     string            `json:"packageId"`
	Params map[string]string `json:"parameters"`
}

func (s *Service) PowerState(state PowerState, servers ...string) ([]*status.QueuedResponse, error) {
	url := fmt.Sprintf("%s/operations/%s/servers/%s", s.config.BaseURL, s.config.Alias, state)
	var resp []*status.QueuedResponse
	err := s.client.Post(url, servers, &resp)
	return resp, err
}

type PowerState int

const (
	On = iota
	Off
	Pause
	Reboot
	Reset
	ShutDown
	StartMaintenance
	StopMaintenance
)

func (p PowerState) String() string {
	switch p {
	case On:
		return "powerOn"
	case Off:
		return "powerOff"
	case Pause:
		return "pause"
	case Reboot:
		return "reboot"
	case Reset:
		return "reset"
	case ShutDown:
		return "shutDown"
	case StartMaintenance:
		return "startMaintenance"
	case StopMaintenance:
		return "stopMaintenance"
	}
	return ""
}

func (s *Service) GetPublicIP(name string, ip string) (*PublicIP, error) {
	url := fmt.Sprintf("%s/servers/%s/%s/publicIPAddresses/%s", s.config.BaseURL, s.config.Alias, name, ip)
	resp := &PublicIP{}
	err := s.client.Get(url, resp)
	return resp, err
}

func (s *Service) AddPublicIP(name string, ip PublicIP) (*status.Status, error) {
	url := fmt.Sprintf("%s/servers/%s/%s/publicIPAddresses", s.config.BaseURL, s.config.Alias, name)
	resp := &status.Status{}
	err := s.client.Post(url, ip, resp)
	return resp, err
}

func (s *Service) UpdatePublicIP(name string, public string, ip PublicIP) (*status.Status, error) {
	url := fmt.Sprintf("%s/servers/%s/%s/publicIPAddresses/%s", s.config.BaseURL, s.config.Alias, name, public)
	resp := &status.Status{}
	err := s.client.Put(url, ip, resp)
	return resp, err
}

func (s *Service) DeletePublicIP(name, ip string) (*status.Status, error) {
	url := fmt.Sprintf("%s/servers/%s/%s/publicIPAddresses/%s", s.config.BaseURL, s.config.Alias, name, ip)
	resp := &status.Status{}
	err := s.client.Delete(url, resp)
	return resp, err
}

type PublicIP struct {
	InternalIP         string              `json:"internalIPAddress,omitempty"`
	Ports              []Port              `json:"ports,omitempty"`
	SourceRestrictions []SourceRestriction `json:"sourceRestrictions,omitempty"`
}

type Port struct {
	Protocol string `json:"protocol"`
	Port     int    `json:"port"`
	PortTo   int    `json:"portTo,omitempty"`
}

type SourceRestriction struct {
	CIDR string `json:"cidr"`
}

type Disk struct {
	DiskID string `json:"diskId,omitempty"`
	Path   string `json:"path,omitempty"`
	SizeGB int    `json:"sizeGB,omitempty"`
	Type   string `json:"type,omitempty"`
}

func (s *Service) AddSecondaryNetwork(name, networkId, ip string) (*status.Status, error) {
	url := fmt.Sprintf("%s/servers/%s/%s/networks", s.config.BaseURL, s.config.Alias, name)
	req := &SecondaryNetwork{
		NetworkID: networkId,
		IPAddress: ip,
	}
	// returned a non-standard status object, repackage into a proper one
	resp := &status.QueuedOperation{}
	err := s.client.Post(url, req, resp)
	if err != nil {
		return nil, err
	}
	return resp.Status(), nil
}

type SecondaryNetwork struct {
	NetworkID string `json:"networkId,omitempty"`
	IPAddress string `json:"ipAddress,omitempty"`
}

func UpdateCPU(num int) api.Update {
	return api.Update{
		Op:     "set",
		Member: "cpu",
		Value:  num,
	}
}

func UpdateMemory(num int) api.Update {
	return api.Update{
		Op:     "set",
		Member: "memory",
		Value:  num,
	}
}

func UpdateCredentials(current, updated string) api.Update {
	return api.Update{
		Op:     "set",
		Member: "password",
		Value: struct {
			Current  string `json:"current"`
			Password string `json:"password"`
		}{
			current,
			updated,
		},
	}
}

func UpdateGroup(group string) api.Update {
	return api.Update{
		Op:     "set",
		Member: "groupId",
		Value:  group,
	}
}

func UpdateDescription(desc string) api.Update {
	return api.Update{
		Op:     "set",
		Member: "description",
		Value:  desc,
	}
}

func UpdateAdditionaldisks(disks []Disk) api.Update {
	return api.Update{
		Op:     "set",
		Member: "disks",
		Value:  disks,
	}
}

func UpdateCustomfields(fields []api.Customfields) api.Update {
	return api.Update{
		Op:     "set",
		Member: "customFields",
		Value:  fields,
	}
}

type Server struct {
	Name                 string             `json:"name"`
	Description          string             `json:"description,omitempty"`
	GroupID              string             `json:"groupId"`
	SourceServerID       string             `json:"sourceServerId"`
	IsManagedOS          bool               `json:"isManagedOS,omitempty"`
	IsManagedBackup      bool               `json:"isManagedBackup,omitempty"`
	PrimaryDNS           string             `json:"primaryDns,omitempty"`
	SecondaryDNS         string             `json:"secondaryDns,omitempty"`
	NetworkID            string             `json:"networkId,omitempty"`
	IPaddress            string             `json:"ipAddress,omitempty"`
	Password             string             `json:"password,omitempty"`
	SourceServerPassword string             `json:"sourceServerPassword,omitempty"`
	CPU                  int                `json:"cpu"`
	CPUAutoscalePolicyID string             `json:"cpuAutoscalePolicyId,omitempty"`
	MemoryGB             int                `json:"memoryGB"`
	Type                 string             `json:"type"`
	Storagetype          string             `json:"storageType,omitempty"`
	AntiAffinityPolicyID string             `json:"antiAffinityPolicyId,omitempty"`
	Customfields         []api.Customfields `json:"customFields,omitempty"`
	Additionaldisks      []Disk             `json:"additionalDisks,omitempty"`
	TTL                  *time.Time         `json:"ttl,omitempty"`
	Packages             []Package          `json:"packages,omitempty"`
	ConfigurationID      string             `json:"configurationId,omitempty"`
	OSType               string             `json:"osType,omitempty"`
}

func (s *Server) Valid() bool {
	return s.Name != "" && s.CPU != 0 && s.MemoryGB != 0 && s.GroupID != "" && s.SourceServerID != "" && s.Type != ""
}

type Response struct {
	ID          string `json:"id"`
	Name        string `json:"name"`
	Description string `json:"description"`
	GroupID     string `json:"groupId"`
	IsTemplate  bool   `json:"isTemplate"`
	LocationID  string `json:"locationId"`
	OStype      string `json:"osType"`
	OS          string `json:"os"`
	Status      string `json:"status"`
	Details     struct {
		IPaddresses []struct {
			Internal string `json:"internal"`
			Public   string `json:"public"`
		} `json:"ipAddresses"`
		AlertPolicies []struct {
			ID    string    `json:"id"`
			Name  string    `json:"name"`
			Links api.Links `json:"links"`
		} `json:"alertPolicies"`
		CPU               int    `json:"cpu"`
		Diskcount         int    `json:"diskCount"`
		Hostname          string `json:"hostName"`
		InMaintenanceMode bool   `json:"inMaintenanceMode"`
		MemoryMB          int    `json:"memoryMB"`
		Powerstate        string `json:"powerState"`
		Storagegb         int    `json:"storageGB"`
		Disks             []struct {
			ID             string        `json:"id"`
			SizeGB         int           `json:"sizeGB"`
			PartitionPaths []interface{} `json:"partitionPaths"`
		} `json:"disks"`
		Partitions []struct {
			SizeGB float64 `json:"sizeGB"`
			Path   string  `json:"path"`
		} `json:"partitions"`
		Snapshots []struct {
			Name  string    `json:"name"`
			Links api.Links `json:"links"`
		} `json:"snapshots"`
		Customfields []api.Customfields `json:"customFields,omitempty"`
	} `json:"details"`
	Type        string `json:"type"`
	Storagetype string `json:"storageType"`
	ChangeInfo  struct {
		CreatedDate  string `json:"createdDate"`
		CreatedBy    string `json:"createdBy"`
		ModifiedDate string `json:"modifiedDate"`
		ModifiedBy   string `json:"modifiedBy"`
	} `json:"changeInfo"`
	Links api.Links `json:"links"`
}
<|endoftext|>