package server import ( "archive/zip" "crypto/md5" "encoding/hex" "fmt" "io" "net/http" "net/url" "os" "path" "reflect" "runtime/debug" "strings" "repositories.action2quare.com/ayo/gocommon" "repositories.action2quare.com/ayo/gocommon/flagx" "repositories.action2quare.com/ayo/gocommon/logger" ) type HoustonServerWithHandler interface { HoustonServer RegisterHandlers(serveMux gocommon.ServerMuxInterface, prefix string) error } type houstonHandler struct { HoustonServer methods map[string]reflect.Method deployPath string downloadPath string maingateApiToken string } func NewHoustonHandler() HoustonServerWithHandler { var tmp *houstonHandler methods := make(map[string]reflect.Method) tp := reflect.TypeOf(tmp) for i := 0; i < tp.NumMethod(); i++ { method := tp.Method(i) methods[strings.ToLower(method.Name)] = method } return &houstonHandler{ HoustonServer: NewServer(), methods: methods, maingateApiToken: loadServerConfig().MaingateApiToken, } } func (h *houstonHandler) RegisterHandlers(serveMux gocommon.ServerMuxInterface, prefix string) error { config := loadServerConfig() storagePath := config.StorageRoot h.deployPath = path.Join(storagePath, sub_folder_name_deploys) h.downloadPath = path.Join(storagePath, sub_folder_name_downloads) if err := os.MkdirAll(h.deployPath, 0775); err != nil { return err } if err := os.MkdirAll(h.downloadPath, 0775); err != nil { return err } if len(prefix) > 0 { prefix = "/" + prefix } serveMux.Handle(prefix, h) fsx := http.FileServer(http.Dir(h.deployPath)) deployPrefix := fmt.Sprintf("%s/%s/", prefix, sub_folder_name_deploys) logger.Printf("houstonHandler registed. deployPath : %s -> %s", fmt.Sprintf("%s/%s/", prefix, sub_folder_name_deploys), h.deployPath) serveMux.HandleFunc(fmt.Sprintf("%s/%s/", prefix, sub_folder_name_deploys), func(w http.ResponseWriter, r *http.Request) { p := strings.TrimPrefix(r.URL.Path, deployPrefix) rp := strings.TrimPrefix(r.URL.RawPath, deployPrefix) h := md5.New() src := strings.TrimLeft(r.URL.Path, fmt.Sprintf("/%s/", prefix)) h.Write([]byte(src)) at := hex.EncodeToString(h.Sum(nil)) if len(p) < len(r.URL.Path) && (r.URL.RawPath == "" || len(rp) < len(r.URL.RawPath)) && at == r.Header.Get("As-X-UrlHash") { r2 := new(http.Request) *r2 = *r r2.URL = new(url.URL) *r2.URL = *r.URL r2.URL.Path = p r2.URL.RawPath = rp fsx.ServeHTTP(w, r2) } else { http.NotFound(w, r) } }) // config는 접근하기 편하게 단축 경로 제공 serveMux.HandleFunc("/config/", func(w http.ResponseWriter, r *http.Request) { logger.Println("config url.path :", r.URL.Path) testhash := md5.New() testhash.Write([]byte(r.URL.Path)) at := hex.EncodeToString(testhash.Sum(nil)) hash := r.Header.Get("As-X-UrlHash") logger.Println("config at = hash :", at, hash) if at == hash { urlpath := strings.TrimPrefix(r.URL.Path, "/config/") dir := path.Dir(urlpath) file := path.Base(urlpath) sourceFile := path.Join(h.deployPath, dir, "config", file) logger.Println("config dest :", sourceFile) bt, err := os.ReadFile(sourceFile) if err != nil && !os.IsExist(err) { logger.Println("config file is missing :", sourceFile) w.WriteHeader(http.StatusNotFound) } else { if _, err = w.Write(bt); err != nil { logger.Println("config write failed :", err) w.WriteHeader(http.StatusInternalServerError) } } } else { http.NotFound(w, r) } }) ufsx := http.FileServer(http.Dir(h.downloadPath)) serveMux.Handle(fmt.Sprintf("%s/%s/", prefix, sub_folder_name_downloads), http.StripPrefix(fmt.Sprintf("%s/%s/", prefix, sub_folder_name_downloads), ufsx)) serveMux.HandleFunc(fmt.Sprintf("%s/upload", prefix), func(w http.ResponseWriter, r *http.Request) { defer func() { s := recover() if s != nil { logger.Error(s) } io.Copy(io.Discard, r.Body) r.Body.Close() }() name := r.Header.Get("Houston-Service-Name") version := r.Header.Get("Houston-Service-Version") filename := r.Header.Get("Houston-Service-Filename") dir := path.Join(h.downloadPath, name, version) if err := os.MkdirAll(dir, 0775); err == nil { zipfile, _ := os.Create(path.Join(dir, filename)) logger.Println("file uploaded :", zipfile) if zipfile != nil { if _, err = io.Copy(zipfile, r.Body); err != nil { w.WriteHeader(http.StatusInternalServerError) } else { if strings.HasSuffix(filename, ".zip") { stat, _ := zipfile.Stat() zipreader, _ := zip.NewReader(zipfile, stat.Size()) for _, f := range zipreader.File { file, _ := os.Create(path.Join(dir, f.Name)) comp, _ := f.Open() io.Copy(file, comp) file.Close() } defer os.Remove(path.Join(dir, filename)) } } zipfile.Close() } else { w.WriteHeader(http.StatusInternalServerError) } } else { w.WriteHeader(http.StatusInternalServerError) } }) return nil } var noauth = flagx.Bool("noauth", false, "") var authtype = flagx.String("auth", "on", "on|off|both") func (h *houstonHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { defer func() { s := recover() if s != nil { logger.Println(s) debug.PrintStack() } }() defer func() { io.Copy(io.Discard, r.Body) r.Body.Close() }() // TODO : 구글 인증까지 붙인 후에 주석 제거 // var userinfo map[string]any // if !*noauth && (*authtype == "on" || *authtype == "both") { // authheader := r.Header.Get("Authorization") // if len(authheader) == 0 { // logger.Println("Authorization header is not valid :", authheader) // w.WriteHeader(http.StatusBadRequest) // return // } // req, _ := http.NewRequest("GET", "https://graph.microsoft.com/oidc/userinfo", nil) // req.Header.Add("Authorization", authheader) // client := &http.Client{} // resp, err := client.Do(req) // if err != nil { // logger.Println("graph microsoft api call failed :", err) // w.WriteHeader(http.StatusBadRequest) // return // } // defer resp.Body.Close() // raw, _ := io.ReadAll(resp.Body) // if err = json.Unmarshal(raw, &userinfo); err != nil { // return // } // if _, expired := userinfo["error"]; expired { // w.WriteHeader(http.StatusUnauthorized) // return // } // } var operation string if r.Method == "POST" { operation = r.FormValue("operation") } else { operation = r.URL.Query().Get("operation") } if len(operation) == 0 { w.WriteHeader(http.StatusBadRequest) return } method, ok := h.methods[strings.ToLower(operation)] if !ok { // 없는 operation logger.Println("fail to call api. operation is not valid :", operation) w.WriteHeader(http.StatusBadRequest) return } args := []reflect.Value{ reflect.ValueOf(h), reflect.ValueOf(w), reflect.ValueOf(r), } w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Headers", "Content-Type") method.Func.Call(args) }