//////////////////////// // // // Indigo // // The Miiverse clone // // that will end all // // other Miiverse // // clones, for real // // this time. // // // // Lead Devs: PF2M, // // Seth/EnergeticBark // // // // Developers: Ben, // // triangles.py, jod, // // & Chance/SRGNation // // // // Artwork: Spicy & // // Inverse & Gnarly // // // // Marketing: Pip // // // // Testing: Mippy ♥ // // // // https://github.com // // /PF2M/Indigo // // // //////////////////////// package main // Import dependencies. import ( // Internals "database/sql" "encoding/json" "html/template" "log" "net" "net/http" "os" "path/filepath" // "user" is already defined in types osUser "os/user" "strconv" "regexp" // Externals "github.com/NYTimes/gziphandler" _ "github.com/go-sql-driver/mysql" "github.com/gorilla/csrf" "github.com/gorilla/mux" "github.com/gorilla/websocket" "github.com/oschwald/geoip2-golang" "github.com/russross/blackfriday/v2" ) // Initialize some variables. var db *sql.DB var err error var clients = make(map[*websocket.Conn]*wsSession) var settings config var admin adminConfig var youtube *regexp.Regexp var spotify *regexp.Regexp var soundcloud *regexp.Regexp var symbols *regexp.Regexp var emotes *regexp.Regexp var renderer *blackfriday.HTMLRenderer var geoip *geoip2.Reader var isGeoIPEnabled bool // Configure the upgrader. var upgrader = websocket.Upgrader{ CheckOrigin: func(r *http.Request) bool { // Todo: Add to this if necessary return true }, EnableCompression: true, } // Define the templates. var templates *template.Template // Redirect HTTP requests to HTTPS if properly configured. func redirect(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, "https://"+r.Host+r.URL.Path, http.StatusTemporaryRedirect) } // Now let's start the main function! func main() { // Fetch the site's settings from JSON files. settings = getSettings() adminJSON, err := os.ReadFile("admin.json") if err != nil { log.Fatal(err) } err = json.Unmarshal(adminJSON, &admin) if err != nil { log.Fatal(err) } // Connect to the database. db, err = sql.Open("mysql", settings.DB.Username+":"+settings.DB.Password+"@tcp("+settings.DB.Host+")/"+settings.DB.Name+"?parseTime=true&loc=US%2FEastern&charset=utf8mb4,utf8") if err != nil { log.Printf("[err]: unable to connect to the database...\n") log.Printf(" %v\n", err) os.Exit(1) } // Ping the database to make sure we connected properly. err = db.Ping() if err != nil { log.Printf("[err]: unable to ping the database...\n") log.Printf(" %v\n", err) os.Exit(1) } _, err = db.Exec("SET CHARACTER SET utf8mb4") if err != nil { log.Printf("[err]: unable to set the character set...\n") log.Printf(" %v\n", err) os.Exit(1) } _, err = db.Exec("SET collation_connection = utf8mb4_bin") if err != nil { log.Printf("[err]: unable to set the connection collation...\n") log.Printf(" %v\n", err) os.Exit(1) } // Initialize some regex. youtube, _ = regexp.Compile("(?:youtube\\.com/\\S*(?:(?:/e(?:mbed))?/|watch/?\\?(?:\\S*?&?v=))|youtu\\.be/)([a-zA-Z0-9_-]{6,11})") spotify, _ = regexp.Compile("(?:embed\\.|open\\.)(?:spotify\\.com/)(?:track/|\\?uri=spotify:track:)((\\w|-){22})") soundcloud, _ = regexp.Compile("(soundcloud\\.com|snd\\.sc)(.*)") symbols, _ = regexp.Compile("(\\|\\\\|`|\\*|{|}|\\[|\\](|)|\\+|-|!|_|>|\\n|&|:|<)") emotes, err = regexp.Compile(":([^ :]+):") if err != nil { log.Fatal(err) } // Initialize Markdown renderer. renderer = blackfriday.NewHTMLRenderer(blackfriday.HTMLRendererParameters{ Flags: 2 | 4 | 128, }) // Initialize GeoIP if a database is present. isGeoIPEnabled = false if _, err = os.Stat("geoip.mmdb"); err == nil { geoip, err = geoip2.Open("geoip.mmdb") if err != nil { log.Fatal(err) } defer geoip.Close() isGeoIPEnabled = true } // Wipe the online statuses of all the users and delete all session keys (necessary after crashes, shutdowns, etc.) db.QueryRow("UPDATE users SET online = 0").Scan() db.QueryRow("TRUNCATE TABLE sessions").Scan() // Close the database connection after this function exits. defer db.Close() // initialize the templates by parsing everything from the views directory recursively var tmplFiles []string err = filepath.Walk("views", func(path string, info os.FileInfo, err error) error { if err != nil { return err } // exclude non-html files if !info.IsDir() && filepath.Ext(path) == ".html" { // feel free to instead make this directly build the template tmplFiles = append(tmplFiles, path) } return nil }) if err != nil { log.Fatal("could not add or find templates (they are stored in views, is this accessible?): ", err) } templates = template.Must(template.ParseFiles(tmplFiles...)) // make the directory for the local image provider if it doesn't exist if settings.ImageHost.Provider == "local" { // check if the error is specifically os.IsNotExist if _, err := os.Stat(settings.ImageHost.ImageEndpoint); os.IsNotExist(err) { // should make it in this working directory err = os.MkdirAll(settings.ImageHost.ImageEndpoint, 0755) if err != nil { log.Println("could not make \""+settings.ImageHost.ImageEndpoint+"\" directory for local image host:", err) } } } // Set up CSRF. CSRF := csrf.Protect([]byte(settings.CSRFSecret), csrf.FieldName("csrfmiddlewaretoken"), csrf.Path("/"), csrf.Secure(settings.SSL.Enabled)) // Initialize routes. r := mux.NewRouter() // functions that don't useLogin or requireLogin, // they don't necessarily not access the user // but just do it independently, not utilizing the CurrentUser // Index route. r.HandleFunc("/", useLogin(index)).Methods("GET") // Auth routes. r.HandleFunc("/signup", signup).Methods("GET", "POST") r.HandleFunc("/login", login).Methods("GET", "POST") r.HandleFunc("/logout", logout).Methods("POST") r.HandleFunc("/reset", useLogin(resetPassword)).Methods("GET", "POST").Queries("token", "{token}") r.HandleFunc("/reset", useLogin(showResetPassword)).Methods("GET", "POST") // User routes. r.HandleFunc("/users", requireLogin(showUserSearch)).Methods("GET").Queries("query", "{username}") r.HandleFunc("/users/{username}", useLogin(showUser)).Methods("GET") r.HandleFunc("/users/{username}/posts", useLogin(showUserPosts)).Methods("GET") r.HandleFunc("/users/{username}/comments", useLogin(showUserComments)).Methods("GET") r.HandleFunc("/users/{username}/yeahs", useLogin(showUserYeahs)).Methods("GET") r.HandleFunc("/users/{username}/friends", useLogin(showFriends)).Methods("GET") r.HandleFunc("/users/{username}/following", useLogin(showFollowing)).Methods("GET") r.HandleFunc("/users/{username}/followers", useLogin(showFollowers)).Methods("GET") r.HandleFunc("/users/{username}/favorites", useLogin(showFavorites)).Methods("GET") r.HandleFunc("/users/{username}/friend_new", requireLogin(newFriendRequest)).Methods("POST") r.HandleFunc("/users/{username}/friend_accept", requireLogin(acceptFriendRequest)).Methods("POST") r.HandleFunc("/users/{username}/friend_reject", requireLogin(rejectFriendRequest)).Methods("POST") r.HandleFunc("/users/{username}/friend_cancel", requireLogin(cancelFriendRequest)).Methods("POST") r.HandleFunc("/users/{username}/friend_delete", requireLogin(deleteFriend)).Methods("POST") r.HandleFunc("/users/{username}/follow", requireLogin(createFollow)).Methods("POST") r.HandleFunc("/users/{username}/unfollow", requireLogin(deleteFollow)).Methods("POST") r.HandleFunc("/users/{username}/violators", requireLogin(reportUser)).Methods("POST") r.HandleFunc("/users/{username}/block", requireLogin(blockUser)).Methods("POST") r.HandleFunc("/users/{username}/unblock", requireLogin(unblockUser)).Methods("POST") // Post routes. r.HandleFunc("/posts/{id:[0-9]+}", useLogin(showPost)).Methods("GET") r.HandleFunc("/posts/{id:[0-9]+}/yeah", requireLogin(createPostYeah)).Methods("POST") r.HandleFunc("/posts/{id:[0-9]+}/yeahu", requireLogin(deletePostYeah)).Methods("POST") r.HandleFunc("/posts/{id:[0-9]+}/comments", useLogin(showAllComments)).Methods("GET") r.HandleFunc("/posts/{id:[0-9]+}/comments", requireLogin(createComment)).Methods("POST") r.HandleFunc("/posts/{id:[0-9]+}/favorite", requireLogin(favoritePost)).Methods("POST") r.HandleFunc("/posts/{id:[0-9]+}/unfavorite", requireLogin(unfavoritePost)).Methods("POST") r.HandleFunc("/posts/{id:[0-9]+}/violations", requireLogin(reportPost)).Methods("POST") r.HandleFunc("/posts/{id:[0-9]+}/vote", requireLogin(voteOnPoll)).Methods("POST") r.HandleFunc("/posts/{id:[0-9]+}/edit", requireLogin(editPost)).Methods("POST") r.HandleFunc("/posts/{id:[0-9]+}/delete", requireLogin(deletePost)).Methods("POST") // Comment routes. r.HandleFunc("/comments/{id:[0-9]+}", useLogin(showComment)).Methods("GET") r.HandleFunc("/comments/{id:[0-9]+}/yeah", requireLogin(createCommentYeah)).Methods("POST") r.HandleFunc("/comments/{id:[0-9]+}/yeahu", requireLogin(deleteCommentYeah)).Methods("POST") r.HandleFunc("/comments/{id:[0-9]+}/violations", requireLogin(reportComment)).Methods("POST") r.HandleFunc("/comments/{id:[0-9]+}/edit", requireLogin(editComment)).Methods("POST") r.HandleFunc("/comments/{id:[0-9]+}/delete", requireLogin(deleteComment)).Methods("POST") // Community routes. r.HandleFunc("/communities/all", useLogin(showAllCommunities)).Methods("GET") r.HandleFunc("/communities/recent", requireLogin(showRecentCommunities)).Methods("GET") r.HandleFunc("/communities/search", useLogin(showCommunitySearch)).Methods("GET").Queries("query", "{search}") r.HandleFunc("/communities/{id:[0-9]+}", useLogin(showCommunity)).Methods("GET") r.HandleFunc("/communities/{id:[0-9]+}/hot", useLogin(showPopularPosts)).Methods("GET") r.HandleFunc("/communities/{id:[0-9]+}/posts", requireLogin(createPost)).Methods("POST") r.HandleFunc("/communities/{id:[0-9]+}/favorite", requireLogin(addCommunityFavorite)).Methods("POST") r.HandleFunc("/communities/{id:[0-9]+}/unfavorite", requireLogin(deleteCommunityFavorite)).Methods("POST") // Activiy Feed route. r.HandleFunc("/activity", requireLogin(showActivityFeed)).Methods("GET") // Message routes. r.HandleFunc("/messages", requireLogin(showMessages)).Methods("GET") r.HandleFunc("/messages", requireLogin(sendMessage)).Methods("POST") r.HandleFunc("/messages/{id:[0-9]+}/delete", requireLogin(deleteMessage)).Methods("POST") r.HandleFunc("/messages/{username}", requireLogin(showConversation)).Methods("GET") r.HandleFunc("/conversations/{id:[0-9]+}", requireLogin(showGroupChat)).Methods("GET") r.HandleFunc("/conversations/create", requireLogin(showCreateGroupChat)).Methods("GET") r.HandleFunc("/conversations/create", requireLogin(createGroupChat)).Methods("POST") r.HandleFunc("/conversations/{id:[0-9]+}/edit", requireLogin(showEditGroupChat)).Methods("GET") r.HandleFunc("/conversations/{id:[0-9]+}/edit", requireLogin(editGroupChat)).Methods("POST") r.HandleFunc("/conversations/{id:[0-9]+}/leave", requireLogin(leaveGroupChat)).Methods("POST") r.HandleFunc("/conversations/{id:[0-9]+}/delete", requireLogin(deleteGroupChat)).Methods("POST") // Notification routes. r.HandleFunc("/check_update.json", requireLogin(getNotificationCounts)).Methods("GET") r.HandleFunc("/notifications", requireLogin(showNotifications)).Methods("GET") r.HandleFunc("/notifications/friend_requests", requireLogin(showFriendRequests)).Methods("GET") // Settings routes. r.HandleFunc("/settings/profile", requireLogin(showProfileSettings)).Methods("GET") r.HandleFunc("/settings/profile", requireLogin(editProfileSettings)).Methods("POST") r.HandleFunc("/region", requireLogin(getRegion)).Methods("POST") r.HandleFunc("/miis", getMii).Methods("POST") r.HandleFunc("/migrate/{id:[0-9]+}", requireLogin(migratePosts)).Methods("POST") r.HandleFunc("/rollback/{id:[0-9]+}", requireLogin(rollbackImport)).Methods("POST") r.HandleFunc("/settings/account", requireLogin(showAccountSettings)).Methods("GET") r.HandleFunc("/settings/account", requireLogin(editAccountSettings)).Methods("POST") r.HandleFunc("/blocked", requireLogin(showBlocked)).Methods("GET") // Help page routes. r.HandleFunc("/help/rules", useLogin(showRulesPage)).Methods("GET") r.HandleFunc("/help/faq", useLogin(showFAQPage)).Methods("GET") r.HandleFunc("/help/legal", useLogin(showLegalPage)).Methods("GET") r.HandleFunc("/help/contact", useLogin(showContactPage)).Methods("GET") // Image upload route. r.HandleFunc("/upload", uploadImage).Methods("POST") // Admin routes. r.HandleFunc("/admin", requireLogin(showAdminDashboard)).Methods("GET") r.HandleFunc("/reports/{id:[0-9]+}/ignore", requireLogin(reportIgnore)).Methods("POST") r.HandleFunc("/admin/manage", requireLogin(showAdminManagerList)).Methods("GET") r.HandleFunc("/admin/manage/bantemp", requireLogin(adminBanUser)).Methods("POST") r.HandleFunc("/admin/manage/unbantemp", requireLogin(adminUnbanUser)).Methods("POST") //r.HandleFunc("/admin/manage/{table}", requireLogin(showAdminManager)).Methods("GET") //r.HandleFunc("/admin/manage/{table}/{id:[0-9]+}", requireLogin(showAdminEditor)).Methods("GET", "POST") r.HandleFunc("/admin/settings", requireLogin(showAdminSettings)).Methods("GET", "POST") r.HandleFunc("/admin/audit_log", requireLogin(showAdminAuditLog)).Methods("GET") // Websocket route. r.HandleFunc("/ws", requireLogin(handleConnections)).Methods("GET") // Add a 404 page. r.NotFoundHandler = useLogin(handle404) // Serve static assets. r.PathPrefix("/assets/").Handler(http.StripPrefix("/assets/", http.FileServer(http.Dir("assets")))) // serve images as /images even though this can be changed r.PathPrefix("/images/").Handler(http.StripPrefix("/images/", http.FileServer(http.Dir("images")))) if !settings.CSRFProtectDisable { r.Use(CSRF) } if settings.GzipEnabled { r.Use(gziphandler.GzipHandler) } // Tell the http server to handle routing with the router we just made. http.Handle("/", r) // Tell the person who started this that we are starting the server. log.Printf("listening on " + settings.Port) // Start the server. if settings.ListenSocket { // remove tha socket first or else os.Remove(settings.Port) unixListener, err := net.Listen("unix", settings.Port) if err != nil { log.Fatal("cannot listen on unix socket: ", err) } // set socket owner but only if the value is not blank if settings.SocketOwner != "" { socketUser, err := osUser.Lookup(settings.SocketOwner) if err != nil { log.Fatal("could not look up user so that we can change the owner of the unix socket so that we can listen on it:\n", err) } // should probably handle errors here uidInt, _ := strconv.Atoi(socketUser.Uid) gidInt, _ := strconv.Atoi(socketUser.Gid) err = os.Chown(settings.Port, uidInt, gidInt) if err != nil { log.Fatal("could not change socket owner", err) } } err = http.Serve(unixListener, nil) // Just serve HTTP requests. if err != nil { log.Fatal(err) } } else { if settings.SSL.Enabled && settings.Port != ":80" { go http.ListenAndServe(":80", http.HandlerFunc(redirect)) // Redirect HTTP requests to the HTTPS site. err = http.ListenAndServeTLS(settings.Port, settings.SSL.Certificate, settings.SSL.Key, nil) if err != nil { log.Fatal(err) } } else { log.Fatal(http.ListenAndServe(settings.Port, nil)) } } }