package main import ( "encoding/json" "flag" "fmt" "log" "net/http" "regexp" "strings" "time" "gitlab.com/mporrato/uBrowserSync/bsync" ) const ( apiVersion = "1.1.13" infoMessage = "Powered by uBrowserSync" defaultMaxSyncSize = 512000 defaultMaxSyncs = 10000 ) const ( statusOnline = 1 statusOffline = 2 statusReadOnly = 3 ) var ( config struct { listen string store string maxSyncSize int maxSyncs int } store bsync.Store serviceStatus = statusOnline sidRe = regexp.MustCompile("^[[:xdigit:]]{32}$") invalidRequestError = bsync.NewSyncError( "Invalid request", "Malformed request body", http.StatusBadRequest) ) func sendJSON(w http.ResponseWriter, status int, data interface{}) { body, err := json.Marshal(data) if err != nil { log.Printf("sendJSON(%v): json.Marshal() failed: %v", data, err) return } w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) _, err = w.Write(body) if err != nil { log.Println("sendJSON() failed:", err) } } func sendJSONOk(w http.ResponseWriter, data interface{}) { sendJSON(w, http.StatusOK, data) } func sendJSONError(w http.ResponseWriter, err error) { log.Println("ERROR: ", err) switch e := err.(type) { case bsync.SyncError: sendJSON(w, e.StatusCode, e.Payload) default: sendJSON(w, http.StatusInternalServerError, bsync.ErrorPayload{ Code: "Internal server error", Message: e.Error()}) } } func info(w http.ResponseWriter, req *http.Request) { if req.Method != "GET" || req.URL.Path != "/info" { sendJSONError(w, bsync.NotImplementedError) } else { log.Println("info()") serviceInfo := bsync.ServiceInfoResp{ Version: apiVersion, Message: infoMessage, Status: serviceStatus, MaxSyncSize: config.maxSyncSize} sendJSONOk(w, serviceInfo) } } func ensureJSONRequest(w http.ResponseWriter, req *http.Request) bool { contentType := strings.Split(strings.ToLower(req.Header["Content-Type"][0]), ";")[0] if contentType != "application/json" { sendJSONError(w, bsync.RequiredDataNotFoundError) return false } return true } func createSync(w http.ResponseWriter, req *http.Request) { if serviceStatus != statusOnline { sendJSONError(w, bsync.NewSyncsForbiddenError) return } if !ensureJSONRequest(w, req) { return } body := new(bsync.CreateReq) req.Body = http.MaxBytesReader(w, req.Body, 10000) err := json.NewDecoder(req.Body).Decode(&body) if err != nil { sendJSONError(w, invalidRequestError) return } if len(body.Version) > 20 { sendJSONError(w, invalidRequestError) return } resp, err := store.Create(body.Version) if err != nil { sendJSONError(w, err) return } if config.maxSyncs > 0 && store.Count() >= config.maxSyncs { serviceStatus = statusReadOnly } sendJSONOk(w, resp) } func getSync(syncId string, w http.ResponseWriter, _ *http.Request) { resp, err := store.Get(syncId) if err != nil { sendJSONError(w, err) return } sendJSONOk(w, resp) } func getLastUpdated(syncId string, w http.ResponseWriter, _ *http.Request) { resp, err := store.GetLastUpdated(syncId) if err != nil { sendJSONError(w, err) return } sendJSONOk(w, bsync.LastUpdatedResp{LastUpdated: resp}) } func getVersion(syncId string, w http.ResponseWriter, _ *http.Request) { resp, err := store.GetVersion(syncId) if err != nil { sendJSONError(w, err) return } sendJSONOk(w, bsync.GetSyncVerResp{Version: resp}) } func updateSync(syncId string, w http.ResponseWriter, req *http.Request) { if !ensureJSONRequest(w, req) { return } body := new(bsync.UpdateReq) req.Body = http.MaxBytesReader(w, req.Body, int64(10000+config.maxSyncSize)) err := json.NewDecoder(req.Body).Decode(&body) if err != nil { sendJSONError(w, invalidRequestError) return } if len(body.Bookmarks) > config.maxSyncSize { sendJSONError(w, bsync.SyncDataLimitExceededError) return } resp, err := store.Update(syncId, body.Bookmarks, body.LastUpdated) if err != nil { sendJSONError(w, err) return } sendJSONOk(w, bsync.UpdateResp{LastUpdated: resp}) } func bookmarks(w http.ResponseWriter, req *http.Request) { elements := strings.Split(strings.Trim(req.URL.Path, "/"), "/") if len(elements) == 1 { if req.Method == "POST" { log.Println("createSync()") createSync(w, req) return } else { sendJSONError(w, bsync.MethodNotImplementedError) return } } syncId := elements[1] if !sidRe.MatchString(syncId) { sendJSONError(w, bsync.NotImplementedError) return } if len(elements) == 2 { if req.Method == "GET" { log.Printf("getSync(%s)", syncId) getSync(syncId, w, req) return } if req.Method == "PUT" { log.Printf("updateSync(%s)", syncId) updateSync(syncId, w, req) return } // TODO: Handle HEAD requests sendJSONError(w, bsync.MethodNotImplementedError) return } if len(elements) == 3 { if elements[2] == "lastUpdated" { if req.Method == "GET" { log.Printf("getLastUpdated(%s)", syncId) getLastUpdated(syncId, w, req) return } sendJSONError(w, bsync.MethodNotImplementedError) return } if elements[2] == "version" { if req.Method == "GET" { log.Printf("getVersion(%s)", syncId) getVersion(syncId, w, req) return } sendJSONError(w, bsync.MethodNotImplementedError) return } } sendJSONError(w, bsync.NotImplementedError) } func notFound(w http.ResponseWriter, _ *http.Request) { sendJSONError(w, bsync.NotImplementedError) } func init() { var ( err error storeDrv bsync.StoreDriver ) flag.StringVar(&config.listen, "listen", ":8090", "listen address and port") flag.StringVar(&config.store, "store", "fs:data", "blob store driver") flag.IntVar(&config.maxSyncSize, "maxsize", defaultMaxSyncSize, "maximum size of a sync in bytes") flag.IntVar(&config.maxSyncs, "maxsyncs", defaultMaxSyncs, "maximum number of syncs") flag.Parse() storeFlagTokens := strings.Split(config.store, ":") switch storeFlagTokens[0] { case "fs": if len(storeFlagTokens) != 2 { err = fmt.Errorf("argument required for fs store driver") } else { storeDrv, err = bsync.NewFSStore(storeFlagTokens[1]) } case "mem": storeDrv, err = bsync.NewMemStore() default: err = fmt.Errorf("Invalid store driver: " + storeFlagTokens[0]) } if err != nil { log.Fatalf("store initialization failed: %v", err) } store = bsync.NewStore(storeDrv) } func main() { mux := http.NewServeMux() mux.HandleFunc("/", notFound) mux.HandleFunc("/info", info) mux.HandleFunc("/info/", info) mux.HandleFunc("/bookmarks", bookmarks) mux.HandleFunc("/bookmarks/", bookmarks) log.Println("HTTP server listening on", config.listen) server := &http.Server{ Addr: config.listen, Handler: mux, ReadTimeout: 15 * time.Second, WriteTimeout: 15 * time.Second, MaxHeaderBytes: 5000} err := server.ListenAndServe() log.Println("HTTP server terminated", err) }