diff --git a/proxy/.env b/proxy/.env index 6416824..c03dad8 100644 --- a/proxy/.env +++ b/proxy/.env @@ -1,4 +1,5 @@ -CLIENT_ID= -CLIENT_SECRET= -REDIRECT_URI= -HOST= \ No newline at end of file +CLIENT_ID=34592 +CLIENT_SECRET=fjGxxsmP9YdlQDh2qOqHsGEaitZlaQHZ2lVoO6n6 +REDIRECT_URI=https://proxy.illegalesachen.download +HOST=https://proxy.illegalesachen.download +PORT=:5163 diff --git a/proxy/.gitignore b/proxy/.gitignore index e69de29..3083d98 100644 --- a/proxy/.gitignore +++ b/proxy/.gitignore @@ -0,0 +1,4 @@ +.env + +tmp/ +*.db diff --git a/proxy/auth.go b/proxy/auth.go index 69c8953..3690c92 100644 --- a/proxy/auth.go +++ b/proxy/auth.go @@ -1,9 +1,13 @@ package main import ( + "bytes" + "encoding/json" "errors" "fmt" + "io" "net/http" + "net/url" "os" "strings" "time" @@ -13,9 +17,6 @@ const ( OsuApiUrl = "https://osu.ppy.sh/api/v2" ) -var clientid = os.Getenv("CLIENT_ID") -var clientsecret = os.Getenv("CLIENT_SECRET") -var redirect_uri = os.Getenv("REDIRECT_URI") var scopes = []string{ "public", "identify", @@ -34,7 +35,7 @@ func NewOsuApiClient(user User) (*OsuApiClient, error) { } if time.Now().After(user.ExpireDate) { - + //request new token? } return &OsuApiClient{ @@ -45,9 +46,48 @@ func NewOsuApiClient(user User) (*OsuApiClient, error) { }, nil } +func (c *OsuApiClient) Me() (*ApiUser, error) { + + req, err := http.NewRequest("GET", fmt.Sprintf("%s/me", OsuApiUrl), nil) + if err != nil { + return nil, err + } + + res := ApiUser{} + if err := c.sendRequest(req, &res); err != nil { + return nil, err + } + + return &res, nil + +} + +func (c *OsuApiClient) sendRequest(req *http.Request, v interface{}) error { + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Accept", "application/json") + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.User.AccessToken)) + + res, err := c.HTTPclient.Do(req) + if err != nil { + fmt.Println("Error: ", err.Error()) + return err + } + + defer res.Body.Close() + + if res.StatusCode < http.StatusOK || res.StatusCode >= http.StatusBadRequest { + return fmt.Errorf("unknown error, status code: %d", res.StatusCode) + } + + if err = json.NewDecoder(res.Body).Decode(v); err != nil { + return err + } + + return nil +} + func LoginRedirect(w http.ResponseWriter, r *http.Request) { - fmt.Println(r.Context()) cookie, ok := r.Context().Value("cookie").(string) if !ok || cookie == "" { @@ -56,11 +96,248 @@ func LoginRedirect(w http.ResponseWriter, r *http.Request) { return } + var clientid = os.Getenv("CLIENT_ID") + var redirect_uri = os.Getenv("REDIRECT_URI") + "/oauth/code" http.Redirect(w, r, fmt.Sprintf("https://osu.ppy.sh/oauth/authorize?client_id=%s&redirect_uri=%s&response_type=code&scope=%s&state=%s", clientid, redirect_uri, strings.Join(scopes, " "), cookie), - http.StatusPermanentRedirect) + http.StatusTemporaryRedirect) +} + +func Oauth(w http.ResponseWriter, r *http.Request) { + cookie, ok := r.Context().Value("cookie").(string) + + if !ok || cookie == "" { + fmt.Println(cookie, ok) + http.Error(w, "Forbidden", http.StatusForbidden) + return + } + + q := r.URL.Query() + + code := q.Get("code") + state := q.Get("state") + + if state != cookie { + + http.Error(w, "Forbidden", http.StatusForbidden) + return + } + + var clientid = os.Getenv("CLIENT_ID") + var client_secret = os.Getenv("CLIENT_SECRET") + var redirect_uri = os.Getenv("REDIRECT_URI") + "/oauth/code" + //request accesstoken + body := url.Values{ + "client_id": {clientid}, + "client_secret": {client_secret}, + "code": {code}, + "grant_type": {"authorization_code"}, + "redirect_uri": {redirect_uri}, + } + + req, err := http.NewRequest("POST", "https://osu.ppy.sh/oauth/token", bytes.NewBufferString(body.Encode())) + if err != nil { + http.Error(w, "Forbidden", http.StatusForbidden) + return + } + + req.Header.Set("Accept", "application/json") + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + httpclient := &http.Client{ + Timeout: time.Minute, + } + + res, err := httpclient.Do(req) + if err != nil { + http.Error(w, "Forbidden", http.StatusForbidden) + return + } + defer res.Body.Close() + + if res.StatusCode < http.StatusOK || res.StatusCode >= http.StatusBadRequest { + http.Error(w, "Forbidden", http.StatusForbidden) + return + } + + data, err := io.ReadAll(res.Body) + if err != nil { + http.Error(w, "Forbidden", http.StatusForbidden) + return + } + + var authToken AuthToken + + err = json.Unmarshal(data, &authToken) + if err != nil { + fmt.Println(err) + http.Error(w, "Forbidden", http.StatusForbidden) + return + } + + user := User{ + Token: Token{ + ExpireDate: time.Now().Add(time.Second * time.Duration(authToken.ExpiresIn)), + RefreshToken: authToken.RefreshToken, + AccessToken: authToken.AccessToken, + }, + } + + c, err := NewOsuApiClient(user) + if err != nil { + fmt.Println(err) + http.Error(w, "Forbidden", http.StatusForbidden) + return + } + + apiuser, err := c.Me() + if err != nil { + fmt.Println(err) + http.Error(w, "Forbidden", http.StatusForbidden) + return + } + + user.UserID = apiuser.ID + user.Name = apiuser.Username + user.AvatarUrl = apiuser.AvatarURL + + SaveCookie(user.UserID, cookie) + if err = SaveUser(user); err != nil { + fmt.Println(err) + } + + JSONResponse(w, http.StatusCreated, user) +} + +type AuthToken struct { + Tokentype string `json:"token_type"` + ExpiresIn int `json:"expires_in"` + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token"` +} + +type ApiUser struct { + AvatarURL string `json:"avatar_url,omitempty"` + CountryCode string `json:"country_code,omitempty"` + DefaultGroup string `json:"default_group,omitempty"` + ID int `json:"id,omitempty"` + IsActive bool `json:"is_active,omitempty"` + IsBot bool `json:"is_bot,omitempty"` + IsDeleted bool `json:"is_deleted,omitempty"` + IsOnline bool `json:"is_online,omitempty"` + IsSupporter bool `json:"is_supporter,omitempty"` + LastVisit time.Time `json:"last_visit,omitempty"` + PmFriendsOnly bool `json:"pm_friends_only,omitempty"` + ProfileColour string `json:"profile_colour,omitempty"` + Username string `json:"username,omitempty"` + CoverURL string `json:"cover_url,omitempty"` + Discord string `json:"discord,omitempty"` + HasSupported bool `json:"has_supported,omitempty"` + Interests any `json:"interests,omitempty"` + JoinDate time.Time `json:"join_date,omitempty"` + Kudosu struct { + Total int `json:"total,omitempty"` + Available int `json:"available,omitempty"` + } `json:"kudosu,omitempty"` + Location any `json:"location,omitempty"` + MaxBlocks int `json:"max_blocks,omitempty"` + MaxFriends int `json:"max_friends,omitempty"` + Occupation any `json:"occupation,omitempty"` + Playmode string `json:"playmode,omitempty"` + Playstyle []string `json:"playstyle,omitempty"` + PostCount int `json:"post_count,omitempty"` + ProfileOrder []string `json:"profile_order,omitempty"` + Title any `json:"title,omitempty"` + Twitter string `json:"twitter,omitempty"` + Website string `json:"website,omitempty"` + Country Country `json:"country,omitempty"` + Cover struct { + CustomURL string `json:"custom_url,omitempty"` + URL string `json:"url,omitempty"` + ID any `json:"id,omitempty"` + } `json:"cover,omitempty"` + IsRestricted bool `json:"is_restricted,omitempty"` + AccountHistory []any `json:"account_history,omitempty"` + ActiveTournamentBanner any `json:"active_tournament_banner,omitempty"` + Badges []struct { + AwardedAt time.Time `json:"awarded_at,omitempty"` + Description string `json:"description,omitempty"` + Image2XURL string `json:"image@2x_url,omitempty"` + ImageURL string `json:"image_url,omitempty"` + URL string `json:"url,omitempty"` + } `json:"badges,omitempty"` + FavouriteBeatmapsetCount int `json:"favourite_beatmapset_count,omitempty"` + FollowerCount int `json:"follower_count,omitempty"` + GraveyardBeatmapsetCount int `json:"graveyard_beatmapset_count,omitempty"` + Groups []struct { + ID int `json:"id,omitempty"` + Identifier string `json:"identifier,omitempty"` + Name string `json:"name,omitempty"` + ShortName string `json:"short_name,omitempty"` + Description string `json:"description,omitempty"` + Colour string `json:"colour,omitempty"` + } `json:"groups,omitempty"` + LovedBeatmapsetCount int `json:"loved_beatmapset_count,omitempty"` + MonthlyPlaycounts []struct { + StartDate string `json:"start_date,omitempty"` + Count int `json:"count,omitempty"` + } `json:"monthly_playcounts,omitempty"` + Page struct { + HTML string `json:"html,omitempty"` + Raw string `json:"raw,omitempty"` + } `json:"page,omitempty"` + PendingBeatmapsetCount int `json:"pending_beatmapset_count,omitempty"` + PreviousUsernames []any `json:"previous_usernames,omitempty"` + RankedBeatmapsetCount int `json:"ranked_beatmapset_count,omitempty"` + ReplaysWatchedCounts []struct { + StartDate string `json:"start_date,omitempty"` + Count int `json:"count,omitempty"` + } `json:"replays_watched_counts,omitempty"` + ScoresFirstCount int `json:"scores_first_count,omitempty"` + Statistics struct { + Level struct { + Current int `json:"current,omitempty"` + Progress int `json:"progress,omitempty"` + } `json:"level,omitempty"` + Pp float64 `json:"pp,omitempty"` + GlobalRank int `json:"global_rank,omitempty"` + RankedScore int `json:"ranked_score,omitempty"` + HitAccuracy float64 `json:"hit_accuracy,omitempty"` + PlayCount int `json:"play_count,omitempty"` + PlayTime int `json:"play_time,omitempty"` + TotalScore int `json:"total_score,omitempty"` + TotalHits int `json:"total_hits,omitempty"` + MaximumCombo int `json:"maximum_combo,omitempty"` + ReplaysWatchedByOthers int `json:"replays_watched_by_others,omitempty"` + IsRanked bool `json:"is_ranked,omitempty"` + GradeCounts struct { + Ss int `json:"ss,omitempty"` + SSH int `json:"ssh,omitempty"` + S int `json:"s,omitempty"` + Sh int `json:"sh,omitempty"` + A int `json:"a,omitempty"` + } `json:"grade_counts,omitempty"` + Rank struct { + Global int `json:"global,omitempty"` + Country int `json:"country,omitempty"` + } `json:"rank,omitempty"` + } `json:"statistics,omitempty"` + SupportLevel int `json:"support_level,omitempty"` + UserAchievements []struct { + AchievedAt time.Time `json:"achieved_at,omitempty"` + AchievementID int `json:"achievement_id,omitempty"` + } `json:"user_achievements,omitempty"` + RankHistory struct { + Mode string `json:"mode,omitempty"` + Data []int `json:"data,omitempty"` + } `json:"rank_history,omitempty"` +} + +type Country struct { + Code string `json:"code"` + Name string `json:"name"` } diff --git a/proxy/database.db b/proxy/database.db index d6b743e..104ea16 100644 Binary files a/proxy/database.db and b/proxy/database.db differ diff --git a/proxy/db.go b/proxy/db.go index 9ef6bbe..6685d2a 100644 --- a/proxy/db.go +++ b/proxy/db.go @@ -2,6 +2,7 @@ package main import ( "database/sql" + "fmt" "log" "time" @@ -9,21 +10,25 @@ import ( ) type User struct { - UserID int - Name string - AvatarUrl string - EndPoint string - Token + UserID int `json:"id"` + Name string `json:"name"` + AvatarUrl string `json:"avatar_url"` + EndPoint string `json:"endpoint"` + Token `json:"-"` } type Token struct { - AuthToken string - RefreshToken string - ExpireDate time.Time + AccessToken string `json:"-"` + RefreshToken string `json:"-"` + ExpireDate time.Time `json:"-"` } var db *sql.DB +const ( + layout = "2006-01-02 15:04:05.999999999-07:00" +) + func InitDB() { var err error db, err = sql.Open("sqlite3", "database.db") @@ -38,7 +43,7 @@ func InitDB() { name TEXT NOT NULL, endpoint TEXT, avatar_url TEXT, - auth_token TEXT, + access_token TEXT, refresh_token TEXT, expire_date TEXT );` @@ -53,7 +58,7 @@ func InitDB() { CREATE TABLE IF NOT EXISTS cookiejar ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER, - cookie TEXT NOT NULL, + cookie TEXT NOT NULL UNIQUE, FOREIGN KEY(user_id) REFERENCES users(id) On DELETE CASCADE );` @@ -65,22 +70,46 @@ func InitDB() { } func GetUserByCookie(cookie string) (*User, error) { + query := ` - SELECT users.id, users.name, users.endpoint, users.avatar_url, users.auth_token, users.refresh_token, users.expire_date + SELECT users.id, users.name, users.endpoint, users.avatar_url, users.access_token, users.refresh_token, users.expire_date FROM users users - JOIN cookiejar cookie ON users.id = cookie.user_id - WHERE cookie.cookie = ?` + JOIN cookiejar co ON users.id = co.user_id + WHERE co.cookie = ?` row := db.QueryRow(query, cookie) var user User - err := row.Scan(&user.UserID, &user.Name, &user.EndPoint, &user.AvatarUrl, &user.AuthToken, &user.RefreshToken, &user.ExpireDate) + var ExpireStr string + err := row.Scan(&user.UserID, &user.Name, &user.EndPoint, &user.AvatarUrl, &user.AccessToken, &user.RefreshToken, &ExpireStr) if err != nil { + fmt.Println(err) + return &User{}, err + } + + user.ExpireDate, err = time.Parse(layout, ExpireStr) + if err != nil { + fmt.Println(err) return &User{}, err } return &user, nil } +func SaveUser(user User) error { + query := `INSERT INTO users (id, name, endpoint, avatar_url, access_token, refresh_token, expire_date) VALUES (?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(id) DO UPDATE SET + name = excluded.name, + endpoint = excluded.endpoint, + avatar_url = excluded.avatar_url, + access_token = excluded.access_token, + refresh_token = excluded.refresh_token, + expire_date = excluded.expire_date; + ` + + _, err := db.Exec(query, user.UserID, user.Name, user.EndPoint, user.AvatarUrl, user.AccessToken, user.RefreshToken, user.ExpireDate) + return err +} + func SaveCookie(userID int, cookie string) error { query := "INSERT INTO cookiejar (user_id, cookie) VALUES (?, ?)" _, err := db.Exec(query, userID, cookie) @@ -89,7 +118,7 @@ func SaveCookie(userID int, cookie string) error { func UpdateUserTokens(userID int, auth Token) error { query := "UPDATE users SET auth_token = ?, refresh_token = ?, expire_date = ? WHERE id = ?" - _, err := db.Exec(query, auth.AuthToken, auth.RefreshToken, auth.ExpireDate, userID) + _, err := db.Exec(query, auth.AccessToken, auth.RefreshToken, auth.ExpireDate, userID) return err } diff --git a/proxy/main.go b/proxy/main.go index 6b45301..c0499bc 100644 --- a/proxy/main.go +++ b/proxy/main.go @@ -8,11 +8,12 @@ import ( func main() { - if ok := godotenv.Load(); ok != nil { + if ok := godotenv.Load(".env"); ok != nil { panic(".env not found") } InitDB() + err := run() if err != nil { fmt.Println(err) diff --git a/proxy/middleware.go b/proxy/middleware.go index 2413d76..adc7755 100644 --- a/proxy/middleware.go +++ b/proxy/middleware.go @@ -34,7 +34,7 @@ func CookieMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { cookie, err := r.Cookie("session_cookie") - if err != nil || cookie.Value == "" { + if err != nil || cookie.Value == "" || cookie == nil { newCookie := &http.Cookie{ Name: "session_cookie", @@ -46,6 +46,7 @@ func CookieMiddleware(next http.Handler) http.Handler { } http.SetCookie(w, newCookie) + cookie = newCookie } ctx := context.WithValue(r.Context(), "cookie", cookie.Value) diff --git a/proxy/routes.go b/proxy/routes.go index 3e9b5e9..dbaa0dc 100644 --- a/proxy/routes.go +++ b/proxy/routes.go @@ -4,26 +4,34 @@ import ( "encoding/json" "fmt" "net/http" + "os" ) func run() error { + + port := os.Getenv("PORT") mux := http.NewServeMux() mux.Handle("/me", AuthMiddleware(http.HandlerFunc(MeHandler))) mux.Handle("/login", http.HandlerFunc(LoginRedirect)) - fmt.Println("Starting Server on :42000") + mux.Handle("/oauth/code", http.HandlerFunc(Oauth)) + + fmt.Println("Starting Server on", port) //global middleware handler := CookieMiddleware(mux) - return http.ListenAndServe(":42000", handler) + return http.ListenAndServe(port, handler) } func MeHandler(w http.ResponseWriter, r *http.Request) { user := r.Context().Value("user").(*User) + //mask token... + user.Token = Token{} + w.Header().Set("Content-Type", "application/Json") JSONResponse(w, http.StatusOK, user)