diff --git a/cmd/server.go b/cmd/server.go index 648d0d18..88170c9c 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -258,7 +258,9 @@ func (o *serverOption) runE(cmd *cobra.Command, args []string) (err error) { ClientID: o.clientID, ClientSecret: o.clientSecret, }, o.oauthSkipTls) - mux.HandlePath(http.MethodGet, "/token", authHandler.RequestCode) + mux.HandlePath(http.MethodGet, "/oauth2/token", authHandler.RequestCode) + mux.HandlePath(http.MethodGet, "/oauth2/getLocalCode", authHandler.RequestLocalCode) + mux.HandlePath(http.MethodGet, "/oauth2/getUserInfoFromLocalCode", authHandler.RequestLocalToken) mux.HandlePath(http.MethodGet, "/oauth2/callback", authHandler.Callback) } diff --git a/cmd/service_test.go b/cmd/service_test.go index 9c7464bd..70e80952 100644 --- a/cmd/service_test.go +++ b/cmd/service_test.go @@ -1,3 +1,27 @@ +/* +MIT License + +Copyright (c) 2023 API Testing Authors. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + package cmd import ( diff --git a/console/atest-ui/src/App.vue b/console/atest-ui/src/App.vue index de1fac79..27c50b0d 100644 --- a/console/atest-ui/src/App.vue +++ b/console/atest-ui/src/App.vue @@ -292,6 +292,28 @@ watch(viewName, (val) => { }); }) +const deviceAuthActive = ref(0) +const deviceAuthResponse = ref({ + user_code: '', + verification_uri: '', + device_code: '' +}) +const deviceAuthNext = () => { + if (deviceAuthActive.value++ > 2) { + return + } + + if (deviceAuthActive.value === 1) { + fetch('/oauth2/getLocalCode') + .then(API.DefaultResponseProcess) + .then((d) => { + deviceAuthResponse.value = d + }) + } else if (deviceAuthActive.value === 2) { + window.location.href = '/oauth2/getUserInfoFromLocalCode?device_code=' + deviceAuthResponse.value.device_code + } +} + const suiteKinds = [{ "name": "HTTP", }, { @@ -311,7 +333,6 @@ API.GetVersion((d) => { return } - console.log(dirtyVersion) if (dirtyVersion && dirtyVersion.length > 0) { appVersionLink.value = appVersionLink.value + '/commit/' + d.message.replace(dirtyVersion[0], '') } else if (version && version.length > 0) { @@ -491,11 +512,27 @@ API.GetVersion((d) => { title="You need to login first." width="30%" > - - - + + + + + + + + + + + + + +
+ Open this link, and type the code: {{ deviceAuthResponse.user_code }}. Then click the next step button. +
+ Next step +
+
diff --git a/console/atest-ui/vite.config.ts b/console/atest-ui/vite.config.ts index 0e4d59fe..da7895fa 100644 --- a/console/atest-ui/vite.config.ts +++ b/console/atest-ui/vite.config.ts @@ -29,6 +29,10 @@ export default defineConfig({ target: 'http://127.0.0.1:8080', changeOrigin: true, }, + '/oauth': { + target: 'http://127.0.0.1:8080', + changeOrigin: true, + }, }, }, }) diff --git a/pkg/oauth/server.go b/pkg/oauth/server.go index 8a7b24b4..ca02c8e4 100644 --- a/pkg/oauth/server.go +++ b/pkg/oauth/server.go @@ -25,12 +25,12 @@ SOFTWARE. package oauth import ( + "encoding/json" "fmt" "log" "net/http" "context" - "crypto/tls" "github.com/linuxsuren/api-testing/pkg/util" "golang.org/x/oauth2" @@ -55,6 +55,7 @@ func NewAuth(provider OAuthProvider, config oauth2.Config, skipTlsVerify bool) * config.Scopes = provider.MinimalScopes() config.Endpoint.TokenURL = fmt.Sprintf("%s%s", provider.GetServer(), provider.GetTokenURL()) config.Endpoint.AuthURL = fmt.Sprintf("%s%s", provider.GetServer(), provider.GetAuthURL()) + config.Endpoint.DeviceAuthURL = "https://github.com/login/device/code" return &auth{ provider: provider, config: config, @@ -78,13 +79,14 @@ func (a *auth) Callback(w http.ResponseWriter, r *http.Request, pathParams map[s } log.Println("get code", code) - tr := &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: a.skipTlsVerify}, - } - sslcli := &http.Client{Transport: tr} + sslcli := util.TlsAwareHTTPClient(a.skipTlsVerify) ctx := context.WithValue(r.Context(), oauth2.HTTPClient, sslcli) token, err := a.config.Exchange(ctx, code, oauth2.VerifierOption(a.verifier)) + a.getUserInfo(w, r, token, err) +} + +func (a *auth) getUserInfo(w http.ResponseWriter, r *http.Request, token *oauth2.Token, err error) { if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return @@ -102,6 +104,33 @@ func (a *auth) Callback(w http.ResponseWriter, r *http.Request, pathParams map[s http.Redirect(w, r, "/?access_token="+token.AccessToken, http.StatusFound) } +func (a *auth) RequestLocalToken(w http.ResponseWriter, r *http.Request, pathParams map[string]string) { + deviceCode := r.FormValue("device_code") + response, ok := deviceAuthResponseMap[deviceCode] + if !ok { + http.Error(w, "device code not found", http.StatusBadRequest) + return + } + + token, err := a.config.DeviceAccessToken(r.Context(), response) + a.getUserInfo(w, r, token, err) +} + +var deviceAuthResponseMap = map[string]*oauth2.DeviceAuthResponse{} + +func (a *auth) RequestLocalCode(w http.ResponseWriter, r *http.Request, pathParams map[string]string) { + response, err := a.config.DeviceAuth(context.Background()) + if err != nil { + log.Println("failed to get device auth", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + deviceAuthResponseMap[response.DeviceCode] = response + + data, _ := json.Marshal(response) + w.Write(data) +} + func (a *auth) RequestCode(w http.ResponseWriter, r *http.Request, pathParams map[string]string) { ref := r.Header.Get("Referer") log.Println("callback host", r.Host) diff --git a/pkg/oauth/user.go b/pkg/oauth/user.go index 4dad6755..0e8d3b3e 100644 --- a/pkg/oauth/user.go +++ b/pkg/oauth/user.go @@ -25,13 +25,14 @@ SOFTWARE. package oauth import ( - "crypto/tls" "encoding/json" "fmt" "io" "log" "net/http" "strings" + + "github.com/linuxsuren/api-testing/pkg/util" ) type OAuthProvider interface { @@ -81,12 +82,7 @@ func GetUserInfo(server OAuthProvider, token string, skipTlsVerify bool) (userIn } req.Header.Set("Authorization", "Bearer "+token) - client := http.Client{ - Transport: &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: skipTlsVerify}, - }, - } - + client := util.TlsAwareHTTPClient(skipTlsVerify) var resp *http.Response if resp, err = client.Do(req); err != nil { return diff --git a/pkg/runner/http.go b/pkg/runner/http.go index e64f1f07..b6572099 100644 --- a/pkg/runner/http.go +++ b/pkg/runner/http.go @@ -26,7 +26,6 @@ package runner import ( "context" - "crypto/tls" "fmt" "io" "log" @@ -131,12 +130,7 @@ func (r *simpleTestCaseRunner) RunTestCase(testcase *testing.TestCase, dataConte } }() - client := http.Client{ - Transport: &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, - }, - } - + client := util.TlsAwareHTTPClient(true) // TODO should have a way to change it contextDir := NewContextKeyBuilder().ParentDir().GetContextValueOrEmpty(ctx) if err = testcase.Request.Render(dataContext, contextDir); err != nil { return @@ -165,7 +159,7 @@ func (r *simpleTestCaseRunner) RunTestCase(testcase *testing.TestCase, dataConte // TODO only do this for unit testing, should remove it once we have a better way if strings.HasPrefix(testcase.Request.API, "http://") { - client = *http.DefaultClient + client = http.DefaultClient } // send the HTTP request diff --git a/pkg/runner/kubernetes/client.go b/pkg/runner/kubernetes/client.go index caa06339..5ae792ea 100644 --- a/pkg/runner/kubernetes/client.go +++ b/pkg/runner/kubernetes/client.go @@ -1,7 +1,6 @@ package kubernetes import ( - "crypto/tls" "encoding/json" "errors" "fmt" @@ -10,6 +9,7 @@ import ( "os" "strings" + "github.com/linuxsuren/api-testing/pkg/util" unstructured "github.com/linuxsuren/unstructured/pkg" ) @@ -64,11 +64,7 @@ var client *http.Client // GetClient returns a default client func GetClient() *http.Client { if client == nil { - client = &http.Client{ - Transport: &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, - }, - } + client = util.TlsAwareHTTPClient(true) // TODO should have a way to change it } return client } diff --git a/pkg/util/http.go b/pkg/util/http.go index 8204e450..f6525324 100644 --- a/pkg/util/http.go +++ b/pkg/util/http.go @@ -26,11 +26,18 @@ package util import ( "bytes" + "crypto/tls" "io" - "io/ioutil" "net/http" ) +func TlsAwareHTTPClient(insecure bool) *http.Client { + tr := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: insecure}, + } + return &http.Client{Transport: tr} +} + func GetDefaultCachedHTTPClient() *http.Client { return &http.Client{ Transport: defaultCachedClient, @@ -47,19 +54,13 @@ type cachedClient struct { cacheData map[string][]byte } -type cachedResponse struct { - body []byte - header http.Header - status int -} - func (c *cachedClient) RoundTrip(req *http.Request) (*http.Response, error) { key := req.URL.String() var cachedData *http.Response var ok bool if cachedData, ok = c.cache[key]; ok { - cachedData.Body = ioutil.NopCloser(bytes.NewReader(c.cacheData[key])) + cachedData.Body = io.NopCloser(bytes.NewReader(c.cacheData[key])) return cachedData, nil } @@ -69,7 +70,7 @@ func (c *cachedClient) RoundTrip(req *http.Request) (*http.Response, error) { } var data []byte if data, err = io.ReadAll(resp.Body); err == nil { - resp.Body = ioutil.NopCloser(bytes.NewReader(data)) + resp.Body = io.NopCloser(bytes.NewReader(data)) } c.cache[key] = resp c.cacheData[key] = data