1 package main 2 3 import ( 4 _ "embed" 5 "io" 6 "net/http" 7 "net/http/httputil" 8 "net/url" 9 "strings" 10 "sync" 11 "sync/atomic" 12 "time" 13 14 "golang.org/x/net/websocket" 15 "vimagination.zapto.org/httpembed" 16 "vimagination.zapto.org/jsonrpc" 17 ) 18 19 var ( 20 //go:embed index.html 21 indexHTML string 22 23 //go:embed auto.js.gz 24 codeJS []byte 25 ) 26 27 type HTTPResponse struct { 28 Code int 29 Message string 30 } 31 32 type serveContents string 33 34 func (s serveContents) ServeHTTP(w http.ResponseWriter, r *http.Request) { 35 http.ServeContent(w, r, r.URL.Path, now, strings.NewReader(string(s))) 36 } 37 38 type wsconn struct { 39 conn *websocket.Conn 40 wg sync.WaitGroup 41 } 42 43 type Server struct { 44 handler http.Handler 45 mux http.ServeMux 46 rproxy atomic.Pointer[httputil.ReverseProxy] 47 rpc atomic.Pointer[jsonrpc.ClientServer] 48 49 mu sync.RWMutex 50 hooks map[string]struct{} 51 wsHooks map[string]*wsconn 52 } 53 54 func newServer(source string) *Server { 55 s := new(Server) 56 57 s.mux.Handle("/", serveContents(indexHTML)) 58 s.mux.Handle("/auto.js", httpembed.HandleBuffer("auto.js", codeJS, 0, time.Now())) 59 s.mux.Handle("/script.js", serveContents(source)) 60 s.mux.Handle("/socket", websocket.Handler(s.intiRPC)) 61 62 return s 63 } 64 65 func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { 66 p := s.rproxy.Load() 67 68 if p != nil { 69 s.handleHooks(w, r, p) 70 } else { 71 s.mux.ServeHTTP(w, r) 72 } 73 } 74 75 type hookRequest struct { 76 URL string `json:"url"` 77 Method string `json:"method"` 78 Headers http.Header `json:"headers"` 79 Body string `json:"body"` 80 } 81 82 type hookResponse struct { 83 Code int `json:"code"` 84 Headers http.Header `json:"headers"` 85 Body string `json:"body"` 86 } 87 88 func (s *Server) handleHooks(w http.ResponseWriter, r *http.Request, p *httputil.ReverseProxy) { 89 if wp := r.Header.Get("Sec-WebSocket-Protocol"); strings.HasPrefix(wp, "X-HOOK-WS:") { 90 s.handleHookedWS(w, r, wp) 91 92 return 93 } 94 95 hookURL := s.getHookURL(r) 96 97 if hookURL != "" { 98 if resp := s.handleHook(w, r, hookURL); resp != nil { 99 w.WriteHeader(resp.Code) 100 101 io.WriteString(w, resp.Message) 102 103 return 104 } 105 } 106 107 p.ServeHTTP(w, r) 108 } 109 110 func (s *Server) handleHookedWS(w http.ResponseWriter, r *http.Request, wp string) { 111 prot := wp 112 113 if pos := strings.IndexByte(wp, ','); pos > 0 { 114 prot = wp[:pos] 115 wp = strings.TrimSpace(wp[pos+1:]) 116 } 117 118 websocket.Handler(func(conn *websocket.Conn) { 119 var justWait bool 120 121 s.mu.Lock() 122 existing := s.wsHooks[prot] 123 if existing == nil { 124 existing = &wsconn{conn: conn} 125 s.wsHooks[prot] = existing 126 justWait = true 127 existing.wg.Add(1) 128 } 129 s.mu.Unlock() 130 131 if justWait { 132 existing.wg.Wait() 133 } else { 134 go io.Copy(existing.conn, conn) 135 io.Copy(conn, existing.conn) 136 137 conn.Close() 138 existing.conn.Close() 139 existing.wg.Done() 140 } 141 }).ServeHTTP(w, r) 142 } 143 144 func (s *Server) getHookURL(r *http.Request) string { 145 if u := r.Header.Get("X-HOOK"); u != "" { 146 up, err := url.Parse(u) 147 if err == nil { 148 r.URL = up 149 } 150 } 151 152 matches := [...]string{ 153 r.URL.String(), 154 r.URL.RequestURI(), 155 r.URL.Path + "?" + r.URL.Query().Encode(), 156 r.URL.Path, 157 } 158 159 s.mu.RLock() 160 defer s.mu.RUnlock() 161 162 for _, u := range matches { 163 if _, ok := s.hooks[u]; ok { 164 return u 165 } 166 } 167 168 return "" 169 } 170 171 func (s *Server) handleHook(w http.ResponseWriter, r *http.Request, hookURL string) *HTTPResponse { 172 rpc := s.rpc.Load() 173 174 if rpc == nil { 175 return &HTTPResponse{http.StatusInternalServerError, "invalid state"} 176 } 177 178 req, err := makeHookRequest(r) 179 if err != nil { 180 return err 181 } 182 183 resp := new(hookResponse) 184 185 if err := rpc.RequestValue(hookURL, req, &resp); err != nil { 186 return &HTTPResponse{http.StatusInternalServerError, err.Error()} 187 } 188 189 if resp != nil { 190 for key, value := range resp.Headers { 191 w.Header()[key] = value 192 } 193 194 return &HTTPResponse{resp.Code, resp.Body} 195 } 196 197 return nil 198 } 199 200 func makeHookRequest(r *http.Request) (hookRequest, *HTTPResponse) { 201 req := hookRequest{ 202 URL: r.URL.String(), 203 Method: r.Method, 204 Headers: r.Header, 205 } 206 207 if r.Body != nil { 208 var sb strings.Builder 209 210 if r.ContentLength > 0 { 211 sb.Grow(int(r.ContentLength)) 212 } 213 214 _, err := io.Copy(&sb, r.Body) 215 r.Body.Close() 216 217 if err != nil { 218 return req, &HTTPResponse{http.StatusBadRequest, err.Error()} 219 } 220 221 req.Body = sb.String() 222 223 r.Body = io.NopCloser(strings.NewReader(req.Body)) 224 } 225 226 return req, nil 227 } 228