2014-07-12 15:45:33 -07:00
|
|
|
// Copyright (C) 2014 Jakob Borg and Contributors (see the CONTRIBUTORS file).
|
2014-09-29 12:43:32 -07:00
|
|
|
//
|
|
|
|
// This program is free software: you can redistribute it and/or modify it
|
|
|
|
// under the terms of the GNU 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 General Public License for
|
|
|
|
// more details.
|
|
|
|
//
|
|
|
|
// You should have received a copy of the GNU General Public License along
|
|
|
|
// with this program. If not, see <http://www.gnu.org/licenses/>.
|
2014-06-01 13:50:14 -07:00
|
|
|
|
2014-03-02 15:58:14 -07:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
2014-08-21 15:45:40 -07:00
|
|
|
"crypto/tls"
|
2014-03-02 15:58:14 -07:00
|
|
|
"encoding/json"
|
2014-05-22 07:12:19 -07:00
|
|
|
"fmt"
|
2014-03-02 15:58:14 -07:00
|
|
|
"io/ioutil"
|
2014-05-22 07:12:19 -07:00
|
|
|
"mime"
|
2014-04-30 13:52:38 -07:00
|
|
|
"net"
|
2014-03-02 15:58:14 -07:00
|
|
|
"net/http"
|
2014-07-22 11:11:36 -07:00
|
|
|
"os"
|
2014-05-22 07:12:19 -07:00
|
|
|
"path/filepath"
|
2014-03-02 15:58:14 -07:00
|
|
|
"runtime"
|
2014-07-13 12:07:24 -07:00
|
|
|
"strconv"
|
2014-07-05 12:40:29 -07:00
|
|
|
"strings"
|
2014-03-02 15:58:14 -07:00
|
|
|
"sync"
|
|
|
|
"time"
|
|
|
|
|
2014-04-19 04:33:51 -07:00
|
|
|
"code.google.com/p/go.crypto/bcrypt"
|
2014-10-26 05:15:14 -07:00
|
|
|
"github.com/calmh/logger"
|
2014-09-22 12:42:11 -07:00
|
|
|
"github.com/syncthing/syncthing/internal/auto"
|
|
|
|
"github.com/syncthing/syncthing/internal/config"
|
2014-10-15 13:52:06 -07:00
|
|
|
"github.com/syncthing/syncthing/internal/discover"
|
2014-09-22 12:42:11 -07:00
|
|
|
"github.com/syncthing/syncthing/internal/events"
|
|
|
|
"github.com/syncthing/syncthing/internal/model"
|
2014-10-06 00:25:45 -07:00
|
|
|
"github.com/syncthing/syncthing/internal/osutil"
|
2014-09-22 12:42:11 -07:00
|
|
|
"github.com/syncthing/syncthing/internal/protocol"
|
|
|
|
"github.com/syncthing/syncthing/internal/upgrade"
|
2014-05-21 11:06:14 -07:00
|
|
|
"github.com/vitrun/qart/qr"
|
2014-03-02 15:58:14 -07:00
|
|
|
)
|
|
|
|
|
|
|
|
type guiError struct {
|
|
|
|
Time time.Time
|
|
|
|
Error string
|
|
|
|
}
|
|
|
|
|
|
|
|
var (
|
|
|
|
configInSync = true
|
|
|
|
guiErrors = []guiError{}
|
|
|
|
guiErrorsMut sync.Mutex
|
2014-07-05 12:40:29 -07:00
|
|
|
modt = time.Now().UTC().Format(http.TimeFormat)
|
2014-07-29 02:06:52 -07:00
|
|
|
eventSub *events.BufferedSubscription
|
2014-03-02 15:58:14 -07:00
|
|
|
)
|
|
|
|
|
2014-05-14 17:08:56 -07:00
|
|
|
func init() {
|
|
|
|
l.AddHandler(logger.LevelWarn, showGuiError)
|
2014-08-05 04:14:04 -07:00
|
|
|
sub := events.Default.Subscribe(events.AllEvents)
|
2014-07-29 02:06:52 -07:00
|
|
|
eventSub = events.NewBufferedSubscription(sub, 1000)
|
2014-05-14 17:08:56 -07:00
|
|
|
}
|
|
|
|
|
2014-05-22 07:12:19 -07:00
|
|
|
func startGUI(cfg config.GUIConfiguration, assetDir string, m *model.Model) error {
|
2014-05-21 05:04:16 -07:00
|
|
|
var err error
|
2014-09-12 12:28:47 -07:00
|
|
|
|
|
|
|
cert, err := loadCert(confDir, "https-")
|
|
|
|
if err != nil {
|
|
|
|
l.Infoln("Loading HTTPS certificate:", err)
|
|
|
|
l.Infoln("Creating new HTTPS certificate")
|
|
|
|
newCertificate(confDir, "https-")
|
|
|
|
cert, err = loadCert(confDir, "https-")
|
|
|
|
}
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
tlsCfg := &tls.Config{
|
|
|
|
Certificates: []tls.Certificate{cert},
|
|
|
|
ServerName: "syncthing",
|
2014-04-30 13:52:38 -07:00
|
|
|
}
|
|
|
|
|
2014-09-14 15:18:05 -07:00
|
|
|
rawListener, err := net.Listen("tcp", cfg.Address)
|
2014-09-12 12:28:47 -07:00
|
|
|
if err != nil {
|
|
|
|
return err
|
2014-04-30 13:52:38 -07:00
|
|
|
}
|
2014-09-14 15:18:05 -07:00
|
|
|
listener := &DowngradingListener{rawListener, tlsCfg}
|
2014-04-30 13:52:38 -07:00
|
|
|
|
2014-07-05 12:40:29 -07:00
|
|
|
// The GET handlers
|
|
|
|
getRestMux := http.NewServeMux()
|
2014-09-18 03:55:28 -07:00
|
|
|
getRestMux.HandleFunc("/rest/ping", restPing)
|
2014-07-29 02:06:52 -07:00
|
|
|
getRestMux.HandleFunc("/rest/completion", withModel(m, restGetCompletion))
|
2014-07-05 12:40:29 -07:00
|
|
|
getRestMux.HandleFunc("/rest/config", restGetConfig)
|
|
|
|
getRestMux.HandleFunc("/rest/config/sync", restGetConfigInSync)
|
2014-07-29 02:06:52 -07:00
|
|
|
getRestMux.HandleFunc("/rest/connections", withModel(m, restGetConnections))
|
2014-07-05 12:40:29 -07:00
|
|
|
getRestMux.HandleFunc("/rest/discovery", restGetDiscovery)
|
2014-07-29 02:06:52 -07:00
|
|
|
getRestMux.HandleFunc("/rest/errors", restGetErrors)
|
2014-07-13 12:07:24 -07:00
|
|
|
getRestMux.HandleFunc("/rest/events", restGetEvents)
|
2014-09-15 15:12:29 -07:00
|
|
|
getRestMux.HandleFunc("/rest/ignores", withModel(m, restGetIgnores))
|
2014-07-26 13:30:29 -07:00
|
|
|
getRestMux.HandleFunc("/rest/lang", restGetLang)
|
2014-07-29 02:06:52 -07:00
|
|
|
getRestMux.HandleFunc("/rest/model", withModel(m, restGetModel))
|
|
|
|
getRestMux.HandleFunc("/rest/need", withModel(m, restGetNeed))
|
2014-09-28 04:00:38 -07:00
|
|
|
getRestMux.HandleFunc("/rest/deviceid", restGetDeviceID)
|
2014-07-29 02:06:52 -07:00
|
|
|
getRestMux.HandleFunc("/rest/report", withModel(m, restGetReport))
|
|
|
|
getRestMux.HandleFunc("/rest/system", restGetSystem)
|
|
|
|
getRestMux.HandleFunc("/rest/upgrade", restGetUpgrade)
|
|
|
|
getRestMux.HandleFunc("/rest/version", restGetVersion)
|
2014-09-28 04:00:38 -07:00
|
|
|
getRestMux.HandleFunc("/rest/stats/device", withModel(m, restGetDeviceStats))
|
2014-07-05 12:40:29 -07:00
|
|
|
|
2014-07-29 04:01:27 -07:00
|
|
|
// Debug endpoints, not for general use
|
|
|
|
getRestMux.HandleFunc("/rest/debug/peerCompletion", withModel(m, restGetPeerCompletion))
|
|
|
|
|
2014-07-05 12:40:29 -07:00
|
|
|
// The POST handlers
|
|
|
|
postRestMux := http.NewServeMux()
|
2014-09-18 03:55:28 -07:00
|
|
|
postRestMux.HandleFunc("/rest/ping", restPing)
|
2014-07-05 12:40:29 -07:00
|
|
|
postRestMux.HandleFunc("/rest/config", withModel(m, restPostConfig))
|
2014-07-29 02:06:52 -07:00
|
|
|
postRestMux.HandleFunc("/rest/discovery/hint", restPostDiscoveryHint)
|
2014-07-05 12:40:29 -07:00
|
|
|
postRestMux.HandleFunc("/rest/error", restPostError)
|
|
|
|
postRestMux.HandleFunc("/rest/error/clear", restClearErrors)
|
2014-09-15 15:12:29 -07:00
|
|
|
postRestMux.HandleFunc("/rest/ignores", withModel(m, restPostIgnores))
|
2014-07-05 12:40:29 -07:00
|
|
|
postRestMux.HandleFunc("/rest/model/override", withModel(m, restPostOverride))
|
2014-07-29 02:06:52 -07:00
|
|
|
postRestMux.HandleFunc("/rest/reset", restPostReset)
|
|
|
|
postRestMux.HandleFunc("/rest/restart", restPostRestart)
|
|
|
|
postRestMux.HandleFunc("/rest/shutdown", restPostShutdown)
|
2014-07-14 01:45:29 -07:00
|
|
|
postRestMux.HandleFunc("/rest/upgrade", restPostUpgrade)
|
2014-08-11 11:20:01 -07:00
|
|
|
postRestMux.HandleFunc("/rest/scan", withModel(m, restPostScan))
|
2014-07-05 12:40:29 -07:00
|
|
|
|
|
|
|
// A handler that splits requests between the two above and disables
|
|
|
|
// caching
|
|
|
|
restMux := noCacheMiddleware(getPostHandler(getRestMux, postRestMux))
|
|
|
|
|
|
|
|
// The main routing handler
|
|
|
|
mux := http.NewServeMux()
|
|
|
|
mux.Handle("/rest/", restMux)
|
|
|
|
mux.HandleFunc("/qr/", getQR)
|
|
|
|
|
|
|
|
// Serve compiled in assets unless an asset directory was set (for development)
|
2014-07-22 11:11:36 -07:00
|
|
|
mux.Handle("/", embeddedStatic(assetDir))
|
2014-05-22 07:12:19 -07:00
|
|
|
|
2014-07-06 06:00:44 -07:00
|
|
|
// Wrap everything in CSRF protection. The /rest prefix should be
|
|
|
|
// protected, other requests will grant cookies.
|
2014-09-01 13:51:44 -07:00
|
|
|
handler := csrfMiddleware("/rest", cfg.APIKey, mux)
|
2014-06-04 12:20:07 -07:00
|
|
|
|
2014-08-31 03:59:20 -07:00
|
|
|
// Add our version as a header to responses
|
|
|
|
handler = withVersionMiddleware(handler)
|
|
|
|
|
2014-07-05 12:40:29 -07:00
|
|
|
// Wrap everything in basic auth, if user/password is set.
|
2014-09-13 14:06:25 -07:00
|
|
|
if len(cfg.User) > 0 && len(cfg.Password) > 0 {
|
2014-09-01 13:51:44 -07:00
|
|
|
handler = basicAuthAndSessionMiddleware(cfg, handler)
|
2014-07-05 12:40:29 -07:00
|
|
|
}
|
2014-04-30 13:52:38 -07:00
|
|
|
|
2014-09-14 15:18:05 -07:00
|
|
|
// Redirect to HTTPS if we are supposed to
|
|
|
|
if cfg.UseTLS {
|
|
|
|
handler = redirectToHTTPSMiddleware(handler)
|
|
|
|
}
|
2014-09-12 12:28:47 -07:00
|
|
|
|
2014-10-13 10:34:26 -07:00
|
|
|
srv := http.Server{
|
|
|
|
Handler: handler,
|
|
|
|
ReadTimeout: 2 * time.Second,
|
|
|
|
}
|
|
|
|
|
2014-09-18 00:27:26 -07:00
|
|
|
go func() {
|
2014-10-13 10:34:26 -07:00
|
|
|
err := srv.Serve(listener)
|
2014-09-18 00:27:26 -07:00
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
}()
|
2014-04-30 13:52:38 -07:00
|
|
|
return nil
|
2014-03-02 15:58:14 -07:00
|
|
|
}
|
|
|
|
|
2014-07-05 12:40:29 -07:00
|
|
|
func getPostHandler(get, post http.Handler) http.Handler {
|
|
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
switch r.Method {
|
|
|
|
case "GET":
|
|
|
|
get.ServeHTTP(w, r)
|
|
|
|
case "POST":
|
|
|
|
post.ServeHTTP(w, r)
|
|
|
|
default:
|
|
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
|
|
}
|
|
|
|
})
|
2014-03-02 15:58:14 -07:00
|
|
|
}
|
|
|
|
|
2014-09-14 15:18:05 -07:00
|
|
|
func redirectToHTTPSMiddleware(h http.Handler) http.Handler {
|
2014-09-12 12:28:47 -07:00
|
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
2014-09-14 15:18:05 -07:00
|
|
|
// Add a generous access-control-allow-origin header since we may be
|
|
|
|
// redirecting REST requests over protocols
|
|
|
|
w.Header().Add("Access-Control-Allow-Origin", "*")
|
|
|
|
|
|
|
|
if r.TLS == nil {
|
|
|
|
// Redirect HTTP requests to HTTPS
|
|
|
|
r.URL.Host = r.Host
|
2014-09-12 12:28:47 -07:00
|
|
|
r.URL.Scheme = "https"
|
|
|
|
http.Redirect(w, r, r.URL.String(), http.StatusFound)
|
|
|
|
} else {
|
|
|
|
h.ServeHTTP(w, r)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2014-07-05 12:40:29 -07:00
|
|
|
func noCacheMiddleware(h http.Handler) http.Handler {
|
|
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
2014-03-26 12:32:35 -07:00
|
|
|
w.Header().Set("Cache-Control", "no-cache")
|
2014-07-05 12:40:29 -07:00
|
|
|
h.ServeHTTP(w, r)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2014-08-31 03:59:20 -07:00
|
|
|
func withVersionMiddleware(h http.Handler) http.Handler {
|
|
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
w.Header().Set("X-Syncthing-Version", Version)
|
|
|
|
h.ServeHTTP(w, r)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2014-07-05 12:40:29 -07:00
|
|
|
func withModel(m *model.Model, h func(m *model.Model, w http.ResponseWriter, r *http.Request)) http.HandlerFunc {
|
|
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
h(m, w, r)
|
2014-03-26 12:32:35 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-09-18 03:55:28 -07:00
|
|
|
func restPing(w http.ResponseWriter, r *http.Request) {
|
|
|
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
|
|
json.NewEncoder(w).Encode(map[string]string{
|
|
|
|
"ping": "pong",
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2014-07-05 12:40:29 -07:00
|
|
|
func restGetVersion(w http.ResponseWriter, r *http.Request) {
|
2014-09-18 03:52:45 -07:00
|
|
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
|
|
json.NewEncoder(w).Encode(map[string]string{
|
|
|
|
"version": Version,
|
|
|
|
"longVersion": LongVersion,
|
|
|
|
"os": runtime.GOOS,
|
|
|
|
"arch": runtime.GOARCH,
|
|
|
|
})
|
2014-03-02 15:58:14 -07:00
|
|
|
}
|
|
|
|
|
2014-07-29 02:06:52 -07:00
|
|
|
func restGetCompletion(m *model.Model, w http.ResponseWriter, r *http.Request) {
|
|
|
|
var qs = r.URL.Query()
|
2014-09-28 04:00:38 -07:00
|
|
|
var folder = qs.Get("folder")
|
|
|
|
var deviceStr = qs.Get("device")
|
2014-07-29 02:06:52 -07:00
|
|
|
|
2014-09-28 04:00:38 -07:00
|
|
|
device, err := protocol.DeviceIDFromString(deviceStr)
|
2014-07-29 02:06:52 -07:00
|
|
|
if err != nil {
|
|
|
|
http.Error(w, err.Error(), 500)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
res := map[string]float64{
|
2014-09-28 04:00:38 -07:00
|
|
|
"completion": m.Completion(device, folder),
|
2014-07-29 02:06:52 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
|
|
json.NewEncoder(w).Encode(res)
|
|
|
|
}
|
|
|
|
|
2014-05-14 20:26:55 -07:00
|
|
|
func restGetModel(m *model.Model, w http.ResponseWriter, r *http.Request) {
|
2014-04-23 01:04:07 -07:00
|
|
|
var qs = r.URL.Query()
|
2014-09-28 04:00:38 -07:00
|
|
|
var folder = qs.Get("folder")
|
2014-03-02 15:58:14 -07:00
|
|
|
var res = make(map[string]interface{})
|
|
|
|
|
2014-10-06 00:25:45 -07:00
|
|
|
res["invalid"] = cfg.Folders()[folder].Invalid
|
2014-04-27 12:53:27 -07:00
|
|
|
|
2014-09-28 04:00:38 -07:00
|
|
|
globalFiles, globalDeleted, globalBytes := m.GlobalSize(folder)
|
2014-03-02 15:58:14 -07:00
|
|
|
res["globalFiles"], res["globalDeleted"], res["globalBytes"] = globalFiles, globalDeleted, globalBytes
|
|
|
|
|
2014-09-28 04:00:38 -07:00
|
|
|
localFiles, localDeleted, localBytes := m.LocalSize(folder)
|
2014-03-02 15:58:14 -07:00
|
|
|
res["localFiles"], res["localDeleted"], res["localBytes"] = localFiles, localDeleted, localBytes
|
|
|
|
|
2014-09-28 04:00:38 -07:00
|
|
|
needFiles, needBytes := m.NeedSize(folder)
|
2014-04-09 13:03:30 -07:00
|
|
|
res["needFiles"], res["needBytes"] = needFiles, needBytes
|
2014-03-02 15:58:14 -07:00
|
|
|
|
2014-04-09 13:03:30 -07:00
|
|
|
res["inSyncFiles"], res["inSyncBytes"] = globalFiles-needFiles, globalBytes-needBytes
|
2014-03-02 15:58:14 -07:00
|
|
|
|
2014-09-28 04:00:38 -07:00
|
|
|
res["state"], res["stateChanged"] = m.State(folder)
|
|
|
|
res["version"] = m.CurrentLocalVersion(folder) + m.RemoteLocalVersion(folder)
|
2014-04-14 00:58:17 -07:00
|
|
|
|
2014-06-22 08:26:31 -07:00
|
|
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
2014-03-02 15:58:14 -07:00
|
|
|
json.NewEncoder(w).Encode(res)
|
|
|
|
}
|
|
|
|
|
2014-07-05 12:40:29 -07:00
|
|
|
func restPostOverride(m *model.Model, w http.ResponseWriter, r *http.Request) {
|
2014-06-16 01:47:02 -07:00
|
|
|
var qs = r.URL.Query()
|
2014-09-28 04:00:38 -07:00
|
|
|
var folder = qs.Get("folder")
|
|
|
|
go m.Override(folder)
|
2014-06-16 01:47:02 -07:00
|
|
|
}
|
|
|
|
|
2014-05-19 13:31:28 -07:00
|
|
|
func restGetNeed(m *model.Model, w http.ResponseWriter, r *http.Request) {
|
|
|
|
var qs = r.URL.Query()
|
2014-09-28 04:00:38 -07:00
|
|
|
var folder = qs.Get("folder")
|
2014-05-19 13:31:28 -07:00
|
|
|
|
2014-09-28 04:39:39 -07:00
|
|
|
files := m.NeedFolderFilesLimited(folder, 100, 2500) // max 100 files or 2500 blocks
|
2014-05-19 13:31:28 -07:00
|
|
|
|
2014-06-22 08:26:31 -07:00
|
|
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
2014-05-19 13:31:28 -07:00
|
|
|
json.NewEncoder(w).Encode(files)
|
|
|
|
}
|
|
|
|
|
2014-07-05 12:40:29 -07:00
|
|
|
func restGetConnections(m *model.Model, w http.ResponseWriter, r *http.Request) {
|
2014-03-02 15:58:14 -07:00
|
|
|
var res = m.ConnectionStats()
|
2014-06-22 08:26:31 -07:00
|
|
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
2014-03-02 15:58:14 -07:00
|
|
|
json.NewEncoder(w).Encode(res)
|
|
|
|
}
|
|
|
|
|
2014-09-28 04:00:38 -07:00
|
|
|
func restGetDeviceStats(m *model.Model, w http.ResponseWriter, r *http.Request) {
|
|
|
|
var res = m.DeviceStatistics()
|
2014-08-21 15:46:34 -07:00
|
|
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
|
|
json.NewEncoder(w).Encode(res)
|
|
|
|
}
|
|
|
|
|
2014-07-05 12:40:29 -07:00
|
|
|
func restGetConfig(w http.ResponseWriter, r *http.Request) {
|
2014-06-22 08:26:31 -07:00
|
|
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
2014-10-06 00:25:45 -07:00
|
|
|
json.NewEncoder(w).Encode(cfg.Raw())
|
2014-03-02 15:58:14 -07:00
|
|
|
}
|
|
|
|
|
2014-07-05 12:40:29 -07:00
|
|
|
func restPostConfig(m *model.Model, w http.ResponseWriter, r *http.Request) {
|
2014-05-24 12:00:47 -07:00
|
|
|
var newCfg config.Configuration
|
2014-07-05 12:40:29 -07:00
|
|
|
err := json.NewDecoder(r.Body).Decode(&newCfg)
|
2014-03-02 15:58:14 -07:00
|
|
|
if err != nil {
|
2014-08-17 01:28:36 -07:00
|
|
|
l.Warnln("decoding posted config:", err)
|
2014-08-08 05:09:27 -07:00
|
|
|
http.Error(w, err.Error(), 500)
|
|
|
|
return
|
2014-03-02 15:58:14 -07:00
|
|
|
} else {
|
2014-10-06 00:25:45 -07:00
|
|
|
if newCfg.GUI.Password != cfg.GUI().Password {
|
2014-09-13 13:52:20 -07:00
|
|
|
if newCfg.GUI.Password != "" {
|
|
|
|
hash, err := bcrypt.GenerateFromPassword([]byte(newCfg.GUI.Password), 0)
|
|
|
|
if err != nil {
|
|
|
|
l.Warnln("bcrypting password:", err)
|
|
|
|
http.Error(w, err.Error(), 500)
|
|
|
|
return
|
|
|
|
} else {
|
|
|
|
newCfg.GUI.Password = string(hash)
|
|
|
|
}
|
2014-04-19 04:33:51 -07:00
|
|
|
}
|
|
|
|
}
|
2014-06-06 19:00:46 -07:00
|
|
|
|
2014-09-20 09:42:09 -07:00
|
|
|
// Start or stop usage reporting as appropriate
|
2014-06-06 19:00:46 -07:00
|
|
|
|
2014-10-06 00:25:45 -07:00
|
|
|
if curAcc := cfg.Options().URAccepted; newCfg.Options.URAccepted > curAcc {
|
2014-06-11 11:04:23 -07:00
|
|
|
// UR was enabled
|
|
|
|
newCfg.Options.URAccepted = usageReportVersion
|
2014-06-11 16:05:00 -07:00
|
|
|
err := sendUsageReport(m)
|
|
|
|
if err != nil {
|
|
|
|
l.Infoln("Usage report:", err)
|
|
|
|
}
|
2014-06-11 11:04:23 -07:00
|
|
|
go usageReportingLoop(m)
|
2014-10-06 00:25:45 -07:00
|
|
|
} else if newCfg.Options.URAccepted < curAcc {
|
2014-06-11 11:04:23 -07:00
|
|
|
// UR was disabled
|
2014-06-17 14:22:19 -07:00
|
|
|
newCfg.Options.URAccepted = -1
|
2014-06-11 11:04:23 -07:00
|
|
|
stopUsageReporting()
|
|
|
|
}
|
|
|
|
|
2014-06-06 19:00:46 -07:00
|
|
|
// Activate and save
|
|
|
|
|
2014-10-07 02:31:19 -07:00
|
|
|
configInSync = !config.ChangeRequiresRestart(cfg.Raw(), newCfg)
|
2014-10-06 00:25:45 -07:00
|
|
|
cfg.Replace(newCfg)
|
|
|
|
cfg.Save()
|
2014-03-02 15:58:14 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-07-05 12:40:29 -07:00
|
|
|
func restGetConfigInSync(w http.ResponseWriter, r *http.Request) {
|
2014-06-22 08:26:31 -07:00
|
|
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
2014-03-02 15:58:14 -07:00
|
|
|
json.NewEncoder(w).Encode(map[string]bool{"configInSync": configInSync})
|
|
|
|
}
|
|
|
|
|
2014-07-05 12:40:29 -07:00
|
|
|
func restPostRestart(w http.ResponseWriter, r *http.Request) {
|
2014-05-12 17:15:18 -07:00
|
|
|
flushResponse(`{"ok": "restarting"}`, w)
|
2014-04-03 13:10:51 -07:00
|
|
|
go restart()
|
|
|
|
}
|
|
|
|
|
2014-07-05 12:40:29 -07:00
|
|
|
func restPostReset(w http.ResponseWriter, r *http.Request) {
|
2014-09-28 04:00:38 -07:00
|
|
|
flushResponse(`{"ok": "resetting folders"}`, w)
|
|
|
|
resetFolders()
|
2014-04-03 13:10:51 -07:00
|
|
|
go restart()
|
2014-03-02 15:58:14 -07:00
|
|
|
}
|
|
|
|
|
2014-07-05 12:40:29 -07:00
|
|
|
func restPostShutdown(w http.ResponseWriter, r *http.Request) {
|
2014-05-12 17:15:18 -07:00
|
|
|
flushResponse(`{"ok": "shutting down"}`, w)
|
2014-05-11 16:16:27 -07:00
|
|
|
go shutdown()
|
|
|
|
}
|
|
|
|
|
2014-05-12 17:15:18 -07:00
|
|
|
func flushResponse(s string, w http.ResponseWriter) {
|
|
|
|
w.Write([]byte(s + "\n"))
|
|
|
|
f := w.(http.Flusher)
|
|
|
|
f.Flush()
|
|
|
|
}
|
|
|
|
|
2014-04-14 03:02:40 -07:00
|
|
|
var cpuUsagePercent [10]float64 // The last ten seconds
|
2014-03-02 15:58:14 -07:00
|
|
|
var cpuUsageLock sync.RWMutex
|
|
|
|
|
2014-07-05 12:40:29 -07:00
|
|
|
func restGetSystem(w http.ResponseWriter, r *http.Request) {
|
2014-03-02 15:58:14 -07:00
|
|
|
var m runtime.MemStats
|
|
|
|
runtime.ReadMemStats(&m)
|
|
|
|
|
2014-10-06 00:25:45 -07:00
|
|
|
tilde, _ := osutil.ExpandTilde("~")
|
2014-03-02 15:58:14 -07:00
|
|
|
res := make(map[string]interface{})
|
2014-06-29 16:42:03 -07:00
|
|
|
res["myID"] = myID.String()
|
2014-03-02 15:58:14 -07:00
|
|
|
res["goroutines"] = runtime.NumGoroutine()
|
|
|
|
res["alloc"] = m.Alloc
|
2014-08-05 13:14:11 -07:00
|
|
|
res["sys"] = m.Sys - m.HeapReleased
|
2014-10-06 00:25:45 -07:00
|
|
|
res["tilde"] = tilde
|
|
|
|
if cfg.Options().GlobalAnnEnabled && discoverer != nil {
|
2014-04-16 08:36:09 -07:00
|
|
|
res["extAnnounceOK"] = discoverer.ExtAnnounceOK()
|
|
|
|
}
|
2014-03-02 15:58:14 -07:00
|
|
|
cpuUsageLock.RLock()
|
2014-04-14 03:02:40 -07:00
|
|
|
var cpusum float64
|
|
|
|
for _, p := range cpuUsagePercent {
|
|
|
|
cpusum += p
|
|
|
|
}
|
2014-03-02 15:58:14 -07:00
|
|
|
cpuUsageLock.RUnlock()
|
2014-04-14 03:02:40 -07:00
|
|
|
res["cpuPercent"] = cpusum / 10
|
2014-03-02 15:58:14 -07:00
|
|
|
|
2014-06-22 08:26:31 -07:00
|
|
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
2014-03-02 15:58:14 -07:00
|
|
|
json.NewEncoder(w).Encode(res)
|
|
|
|
}
|
|
|
|
|
2014-07-05 12:40:29 -07:00
|
|
|
func restGetErrors(w http.ResponseWriter, r *http.Request) {
|
2014-06-22 08:26:31 -07:00
|
|
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
2014-03-02 15:58:14 -07:00
|
|
|
guiErrorsMut.Lock()
|
2014-09-18 03:49:59 -07:00
|
|
|
json.NewEncoder(w).Encode(map[string][]guiError{"errors": guiErrors})
|
2014-03-02 15:58:14 -07:00
|
|
|
guiErrorsMut.Unlock()
|
|
|
|
}
|
|
|
|
|
2014-07-05 12:40:29 -07:00
|
|
|
func restPostError(w http.ResponseWriter, r *http.Request) {
|
|
|
|
bs, _ := ioutil.ReadAll(r.Body)
|
|
|
|
r.Body.Close()
|
2014-05-14 17:08:56 -07:00
|
|
|
showGuiError(0, string(bs))
|
2014-03-02 15:58:14 -07:00
|
|
|
}
|
|
|
|
|
2014-07-05 12:40:29 -07:00
|
|
|
func restClearErrors(w http.ResponseWriter, r *http.Request) {
|
2014-04-16 07:30:49 -07:00
|
|
|
guiErrorsMut.Lock()
|
2014-05-17 04:54:11 -07:00
|
|
|
guiErrors = []guiError{}
|
2014-04-16 07:30:49 -07:00
|
|
|
guiErrorsMut.Unlock()
|
|
|
|
}
|
|
|
|
|
2014-05-14 17:08:56 -07:00
|
|
|
func showGuiError(l logger.LogLevel, err string) {
|
2014-03-02 15:58:14 -07:00
|
|
|
guiErrorsMut.Lock()
|
|
|
|
guiErrors = append(guiErrors, guiError{time.Now(), err})
|
|
|
|
if len(guiErrors) > 5 {
|
|
|
|
guiErrors = guiErrors[len(guiErrors)-5:]
|
|
|
|
}
|
|
|
|
guiErrorsMut.Unlock()
|
|
|
|
}
|
2014-04-19 04:33:51 -07:00
|
|
|
|
2014-07-05 12:40:29 -07:00
|
|
|
func restPostDiscoveryHint(w http.ResponseWriter, r *http.Request) {
|
2014-05-12 17:50:54 -07:00
|
|
|
var qs = r.URL.Query()
|
2014-09-28 04:00:38 -07:00
|
|
|
var device = qs.Get("device")
|
2014-05-12 17:50:54 -07:00
|
|
|
var addr = qs.Get("addr")
|
2014-09-28 04:00:38 -07:00
|
|
|
if len(device) != 0 && len(addr) != 0 && discoverer != nil {
|
|
|
|
discoverer.Hint(device, []string{addr})
|
2014-05-12 17:50:54 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-07-05 12:40:29 -07:00
|
|
|
func restGetDiscovery(w http.ResponseWriter, r *http.Request) {
|
2014-10-15 11:23:52 -07:00
|
|
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
2014-10-28 12:40:04 -07:00
|
|
|
devices := map[string][]discover.CacheEntry{}
|
|
|
|
|
|
|
|
if discoverer != nil {
|
|
|
|
// Device ids can't be marshalled as keys so we need to manually
|
|
|
|
// rebuild this map using strings. Discoverer may be nil if discovery
|
|
|
|
// has not started yet.
|
|
|
|
for device, entries := range discoverer.All() {
|
|
|
|
devices[device.String()] = entries
|
|
|
|
}
|
2014-10-15 11:23:28 -07:00
|
|
|
}
|
2014-10-15 13:52:06 -07:00
|
|
|
|
2014-10-15 11:23:28 -07:00
|
|
|
json.NewEncoder(w).Encode(devices)
|
2014-05-12 18:08:55 -07:00
|
|
|
}
|
|
|
|
|
2014-07-05 12:40:29 -07:00
|
|
|
func restGetReport(m *model.Model, w http.ResponseWriter, r *http.Request) {
|
2014-06-22 08:26:31 -07:00
|
|
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
2014-06-11 11:04:23 -07:00
|
|
|
json.NewEncoder(w).Encode(reportData(m))
|
|
|
|
}
|
|
|
|
|
2014-09-15 15:12:29 -07:00
|
|
|
func restGetIgnores(m *model.Model, w http.ResponseWriter, r *http.Request) {
|
|
|
|
qs := r.URL.Query()
|
|
|
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
|
|
|
2014-09-28 04:00:38 -07:00
|
|
|
ignores, err := m.GetIgnores(qs.Get("folder"))
|
2014-09-15 15:12:29 -07:00
|
|
|
if err != nil {
|
|
|
|
http.Error(w, err.Error(), 500)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
json.NewEncoder(w).Encode(map[string][]string{
|
|
|
|
"ignore": ignores,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
func restPostIgnores(m *model.Model, w http.ResponseWriter, r *http.Request) {
|
|
|
|
qs := r.URL.Query()
|
|
|
|
|
|
|
|
var data map[string][]string
|
|
|
|
err := json.NewDecoder(r.Body).Decode(&data)
|
|
|
|
r.Body.Close()
|
2014-09-19 13:02:53 -07:00
|
|
|
|
2014-09-15 15:12:29 -07:00
|
|
|
if err != nil {
|
|
|
|
http.Error(w, err.Error(), 500)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2014-09-28 04:00:38 -07:00
|
|
|
err = m.SetIgnores(qs.Get("folder"), data["ignore"])
|
2014-09-15 15:12:29 -07:00
|
|
|
if err != nil {
|
|
|
|
http.Error(w, err.Error(), 500)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
restGetIgnores(m, w, r)
|
|
|
|
}
|
|
|
|
|
2014-07-13 12:07:24 -07:00
|
|
|
func restGetEvents(w http.ResponseWriter, r *http.Request) {
|
|
|
|
qs := r.URL.Query()
|
2014-07-29 02:06:52 -07:00
|
|
|
sinceStr := qs.Get("since")
|
|
|
|
limitStr := qs.Get("limit")
|
|
|
|
since, _ := strconv.Atoi(sinceStr)
|
|
|
|
limit, _ := strconv.Atoi(limitStr)
|
|
|
|
|
2014-08-19 15:30:32 -07:00
|
|
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
|
|
|
2014-08-19 15:18:28 -07:00
|
|
|
// Flush before blocking, to indicate that we've received the request
|
|
|
|
// and that it should not be retried.
|
|
|
|
f := w.(http.Flusher)
|
|
|
|
f.Flush()
|
|
|
|
|
2014-07-29 02:06:52 -07:00
|
|
|
evs := eventSub.Since(since, nil)
|
|
|
|
if 0 < limit && limit < len(evs) {
|
|
|
|
evs = evs[len(evs)-limit:]
|
|
|
|
}
|
2014-07-13 12:07:24 -07:00
|
|
|
|
2014-07-29 02:06:52 -07:00
|
|
|
json.NewEncoder(w).Encode(evs)
|
2014-07-13 12:07:24 -07:00
|
|
|
}
|
|
|
|
|
2014-07-14 01:45:29 -07:00
|
|
|
func restGetUpgrade(w http.ResponseWriter, r *http.Request) {
|
2014-07-31 07:01:23 -07:00
|
|
|
rel, err := upgrade.LatestRelease(strings.Contains(Version, "-beta"))
|
2014-07-14 01:45:29 -07:00
|
|
|
if err != nil {
|
|
|
|
http.Error(w, err.Error(), 500)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
res := make(map[string]interface{})
|
|
|
|
res["running"] = Version
|
|
|
|
res["latest"] = rel.Tag
|
2014-07-31 07:01:23 -07:00
|
|
|
res["newer"] = upgrade.CompareVersions(rel.Tag, Version) == 1
|
2014-07-14 01:45:29 -07:00
|
|
|
|
|
|
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
|
|
json.NewEncoder(w).Encode(res)
|
|
|
|
}
|
|
|
|
|
2014-09-28 04:00:38 -07:00
|
|
|
func restGetDeviceID(w http.ResponseWriter, r *http.Request) {
|
2014-07-18 01:00:02 -07:00
|
|
|
qs := r.URL.Query()
|
|
|
|
idStr := qs.Get("id")
|
2014-09-28 04:00:38 -07:00
|
|
|
id, err := protocol.DeviceIDFromString(idStr)
|
2014-07-18 01:00:02 -07:00
|
|
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
|
|
if err == nil {
|
|
|
|
json.NewEncoder(w).Encode(map[string]string{
|
|
|
|
"id": id.String(),
|
|
|
|
})
|
|
|
|
} else {
|
|
|
|
json.NewEncoder(w).Encode(map[string]string{
|
|
|
|
"error": err.Error(),
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-07-26 13:30:29 -07:00
|
|
|
func restGetLang(w http.ResponseWriter, r *http.Request) {
|
|
|
|
lang := r.Header.Get("Accept-Language")
|
|
|
|
var langs []string
|
|
|
|
for _, l := range strings.Split(lang, ",") {
|
2014-08-14 08:04:17 -07:00
|
|
|
parts := strings.SplitN(l, ";", 2)
|
2014-08-28 04:23:23 -07:00
|
|
|
langs = append(langs, strings.ToLower(strings.TrimSpace(parts[0])))
|
2014-07-26 13:30:29 -07:00
|
|
|
}
|
2014-08-05 00:38:38 -07:00
|
|
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
2014-07-26 13:30:29 -07:00
|
|
|
json.NewEncoder(w).Encode(langs)
|
|
|
|
}
|
|
|
|
|
2014-07-14 01:45:29 -07:00
|
|
|
func restPostUpgrade(w http.ResponseWriter, r *http.Request) {
|
2014-07-31 07:01:23 -07:00
|
|
|
rel, err := upgrade.LatestRelease(strings.Contains(Version, "-beta"))
|
2014-07-14 01:45:29 -07:00
|
|
|
if err != nil {
|
2014-08-17 01:28:36 -07:00
|
|
|
l.Warnln("getting latest release:", err)
|
2014-07-14 01:45:29 -07:00
|
|
|
http.Error(w, err.Error(), 500)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2014-07-31 07:01:23 -07:00
|
|
|
if upgrade.CompareVersions(rel.Tag, Version) == 1 {
|
2014-08-07 06:57:20 -07:00
|
|
|
err = upgrade.UpgradeTo(rel, GoArchExtra)
|
2014-07-31 07:01:23 -07:00
|
|
|
if err != nil {
|
2014-08-17 01:28:36 -07:00
|
|
|
l.Warnln("upgrading:", err)
|
2014-07-31 07:01:23 -07:00
|
|
|
http.Error(w, err.Error(), 500)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2014-09-13 07:25:39 -07:00
|
|
|
flushResponse(`{"ok": "restarting"}`, w)
|
|
|
|
l.Infoln("Upgrading")
|
|
|
|
stop <- exitUpgrading
|
2014-07-31 07:01:23 -07:00
|
|
|
}
|
2014-07-14 01:45:29 -07:00
|
|
|
}
|
|
|
|
|
2014-08-11 11:20:01 -07:00
|
|
|
func restPostScan(m *model.Model, w http.ResponseWriter, r *http.Request) {
|
|
|
|
qs := r.URL.Query()
|
2014-09-28 04:00:38 -07:00
|
|
|
folder := qs.Get("folder")
|
2014-08-11 11:20:01 -07:00
|
|
|
sub := qs.Get("sub")
|
2014-09-28 04:00:38 -07:00
|
|
|
err := m.ScanFolderSub(folder, sub)
|
2014-08-11 11:20:01 -07:00
|
|
|
if err != nil {
|
|
|
|
http.Error(w, err.Error(), 500)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-07-05 12:40:29 -07:00
|
|
|
func getQR(w http.ResponseWriter, r *http.Request) {
|
2014-08-04 13:53:37 -07:00
|
|
|
var qs = r.URL.Query()
|
|
|
|
var text = qs.Get("text")
|
2014-07-05 12:40:29 -07:00
|
|
|
code, err := qr.Encode(text, qr.M)
|
2014-05-21 11:06:14 -07:00
|
|
|
if err != nil {
|
|
|
|
http.Error(w, "Invalid", 500)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
w.Header().Set("Content-Type", "image/png")
|
|
|
|
w.Write(code.PNG())
|
|
|
|
}
|
|
|
|
|
2014-07-29 04:01:27 -07:00
|
|
|
func restGetPeerCompletion(m *model.Model, w http.ResponseWriter, r *http.Request) {
|
|
|
|
tot := map[string]float64{}
|
|
|
|
count := map[string]float64{}
|
|
|
|
|
2014-10-06 00:25:45 -07:00
|
|
|
for _, folder := range cfg.Folders() {
|
2014-09-28 04:00:38 -07:00
|
|
|
for _, device := range folder.DeviceIDs() {
|
|
|
|
deviceStr := device.String()
|
|
|
|
if m.ConnectedTo(device) {
|
|
|
|
tot[deviceStr] += m.Completion(device, folder.ID)
|
2014-07-29 04:01:27 -07:00
|
|
|
} else {
|
2014-09-28 04:00:38 -07:00
|
|
|
tot[deviceStr] = 0
|
2014-07-29 04:01:27 -07:00
|
|
|
}
|
2014-09-28 04:00:38 -07:00
|
|
|
count[deviceStr]++
|
2014-07-29 04:01:27 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
comp := map[string]int{}
|
2014-09-28 04:00:38 -07:00
|
|
|
for device := range tot {
|
|
|
|
comp[device] = int(tot[device] / count[device])
|
2014-07-29 04:01:27 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
|
|
json.NewEncoder(w).Encode(comp)
|
|
|
|
}
|
|
|
|
|
2014-07-22 11:11:36 -07:00
|
|
|
func embeddedStatic(assetDir string) http.Handler {
|
2014-09-02 04:07:33 -07:00
|
|
|
assets := auto.Assets()
|
|
|
|
|
2014-07-22 11:11:36 -07:00
|
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
file := r.URL.Path
|
2014-05-22 07:12:19 -07:00
|
|
|
|
2014-07-22 11:11:36 -07:00
|
|
|
if file[0] == '/' {
|
|
|
|
file = file[1:]
|
|
|
|
}
|
2014-05-22 07:12:19 -07:00
|
|
|
|
2014-07-22 11:11:36 -07:00
|
|
|
if len(file) == 0 {
|
|
|
|
file = "index.html"
|
|
|
|
}
|
2014-05-22 07:12:19 -07:00
|
|
|
|
2014-07-22 11:11:36 -07:00
|
|
|
if assetDir != "" {
|
|
|
|
p := filepath.Join(assetDir, filepath.FromSlash(file))
|
|
|
|
_, err := os.Stat(p)
|
|
|
|
if err == nil {
|
|
|
|
http.ServeFile(w, r, p)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
2014-05-22 07:12:19 -07:00
|
|
|
|
2014-09-02 04:07:33 -07:00
|
|
|
bs, ok := assets[file]
|
2014-07-22 11:11:36 -07:00
|
|
|
if !ok {
|
|
|
|
http.NotFound(w, r)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2014-09-03 23:24:42 -07:00
|
|
|
mtype := mimeTypeForFile(file)
|
2014-07-22 11:11:36 -07:00
|
|
|
if len(mtype) != 0 {
|
|
|
|
w.Header().Set("Content-Type", mtype)
|
|
|
|
}
|
|
|
|
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(bs)))
|
|
|
|
w.Header().Set("Last-Modified", modt)
|
2014-07-05 12:40:29 -07:00
|
|
|
|
2014-07-22 11:11:36 -07:00
|
|
|
w.Write(bs)
|
|
|
|
})
|
2014-05-22 07:12:19 -07:00
|
|
|
}
|
2014-09-03 23:24:42 -07:00
|
|
|
|
|
|
|
func mimeTypeForFile(file string) string {
|
|
|
|
// We use a built in table of the common types since the system
|
|
|
|
// TypeByExtension might be unreliable. But if we don't know, we delegate
|
|
|
|
// to the system.
|
|
|
|
ext := filepath.Ext(file)
|
|
|
|
switch ext {
|
|
|
|
case ".htm", ".html":
|
|
|
|
return "text/html"
|
|
|
|
case ".css":
|
|
|
|
return "text/css"
|
|
|
|
case ".js":
|
|
|
|
return "application/javascript"
|
|
|
|
case ".json":
|
|
|
|
return "application/json"
|
|
|
|
case ".png":
|
|
|
|
return "image/png"
|
|
|
|
case ".ttf":
|
|
|
|
return "application/x-font-ttf"
|
2014-09-03 23:47:23 -07:00
|
|
|
case ".woff":
|
|
|
|
return "application/x-font-woff"
|
2014-09-03 23:24:42 -07:00
|
|
|
default:
|
|
|
|
return mime.TypeByExtension(ext)
|
|
|
|
}
|
|
|
|
}
|