├── .gitignore ├── uninstall.sh ├── LICENSE.md ├── install.sh ├── README.md └── http.go /.gitignore: -------------------------------------------------------------------------------- 1 | *.h 2 | *.so 3 | .vscode 4 | -------------------------------------------------------------------------------- /uninstall.sh: -------------------------------------------------------------------------------- 1 | if [[ $# > 0 ]]; then 2 | mysql --user=$1 --password=$2 -s -N -e "DROP FUNCTION http_help;" 3 | mysql --user=$1 --password=$2 -s -N -e "DROP FUNCTION http_raw;" 4 | mysql --user=$1 --password=$2 -s -N -e "DROP FUNCTION http_get;" 5 | mysql --user=$1 --password=$2 -s -N -e "DROP FUNCTION http_post;" 6 | 7 | sql_result=$(mysql --user=$1 --password=$2 -s -N -e "SHOW VARIABLES LIKE 'plugin_dir';") 8 | plugin_dir=$(cut -d" " -f2 <<< $sql_result) 9 | rm $plugin_dir"http.so" 10 | 11 | echo "Uninstall Success" 12 | else 13 | echo "bash uninstall.sh username password(optional)" 14 | fi 15 | 16 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | ## THE BEER-WARE LICENSE (Revision 42) 2 | HellP(2rebi)<> wrote this software. As long as you 3 | retain this notice you can do whatever you want with this stuff. If we meet 4 | some day, and you think this stuff is worth it, you can buy me a beer in return. 5 | 6 | ## 비어웨어 라이센스 (개정 42) 7 | 헬프(이재성)<>이 이 소프트웨어를 작성했습니다. 8 | 당신은 이 통지를 보유하는 한 이 소프트웨어로 무엇이든 할 수 있습니다. 9 | 이게 가치가 있다고 생각하면, 만나서 보답으로 제게 맥주를 사줄 수 있습니다. 10 | 11 | 12 | [비어웨어 라이센스](http://en.wikipedia.org/wiki/Beerware)는 [Poul-Henning Kamp](http://people.freebsd.org/~phk/)에 의해 처음 작성되었습니다. 13 | 14 | [원본 번역문](https://github.com/dolsup/fontler/blob/69bbaaab76804d16abd4755b3045e606951e7148/LICENSE.md)은 [돌숲(최지원)](https://github.com/dolsup)<<1890mah@gmail.com>>으로 부터 허락을 구하고 퍼옴. 15 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | # Determine the name of the MySQL/MariaDB configuration command 2 | get_mysql_config_cmd() { 3 | version_info=$(mysql --version) 4 | if [[ "$version_info" == *"Maria"* ]]; then 5 | echo "mariadb_config" 6 | else 7 | echo "mysql_config" 8 | fi 9 | } 10 | 11 | # Retrieve the directory containing MySQL/MariaDB header files 12 | get_include_dir() { 13 | local mysql_config_cmd 14 | mysql_config_cmd=$(get_mysql_config_cmd) 15 | $mysql_config_cmd --include 16 | } 17 | 18 | # Retrieve the MySQL/MariaDB plugin directory 19 | get_plugin_dir() { 20 | local username=$1 21 | local password=$2 22 | local sql_result 23 | sql_result=$(mysql --user=$username --password=$password -s -N -e "SHOW VARIABLES LIKE 'plugin_dir';") 24 | cut -d" " -f2 <<< $sql_result 25 | } 26 | 27 | # Execute a MySQL/MariaDB command 28 | execute_mysql_cmd() { 29 | local username=$1 30 | local password=$2 31 | local command=$3 32 | mysql --user=$username --password=$password -s -N -e "$command" 33 | } 34 | 35 | # Compile and install the HTTP plugin 36 | install_http_plugin() { 37 | local username=$1 38 | local password=$2 39 | local include_dir 40 | include_dir=$(get_include_dir) 41 | local plugin_dir 42 | plugin_dir=$(get_plugin_dir $username $password) 43 | 44 | export CGO_CFLAGS=$include_dir 45 | go build -buildmode=c-shared -o "$plugin_dir/http.so" http.go 46 | rm "$plugin_dir/http.h" 47 | } 48 | 49 | # Create MySQL/MariaDB functions for the HTTP plugin 50 | create_http_functions() { 51 | local username=$1 52 | local password=$2 53 | execute_mysql_cmd $username $password "CREATE OR REPLACE FUNCTION http_help RETURNS STRING SONAME 'http.so';" 54 | execute_mysql_cmd $username $password "CREATE OR REPLACE FUNCTION http_raw RETURNS STRING SONAME 'http.so';" 55 | execute_mysql_cmd $username $password "CREATE OR REPLACE FUNCTION http_get RETURNS STRING SONAME 'http.so';" 56 | execute_mysql_cmd $username $password "CREATE OR REPLACE FUNCTION http_post RETURNS STRING SONAME 'http.so';" 57 | } 58 | 59 | # Check if the script was called with at least one argument (username) 60 | if [[ $# -lt 1 ]]; then 61 | echo "Error: you must specify the MySQL/MariaDB username as an argument." 62 | echo "Usage: bash install.sh username [password]" 63 | exit 1 64 | fi 65 | 66 | # Retrieve 67 | # Retrieve the username and password (optional) 68 | username=$1 69 | password= 70 | if [[ $# -gt 1 ]]; then 71 | password=$2 72 | fi 73 | 74 | # Install the HTTP plugin and create the MySQL/MariaDB functions 75 | install_http_plugin $username $password 76 | create_http_functions $username $password 77 | 78 | echo "Installation successful" 79 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mysql_udf_http_golang 2 | [![MySQL UDF](https://img.shields.io/badge/MySQL-UDF-blue.svg)](https://dev.mysql.com/) [![MariaDB UDF](https://img.shields.io/badge/MariaDB-UDF-blue.svg)](https://mariadb.com/) 3 | 4 | [MySQL](https://dev.mysql.com/) or [MariaDB](https://mariadb.com/) UDF(User-Defined Functions) HTTP Client Plugin 5 | 6 | Setup 7 | --- 8 | - **Clone Source** 9 | ```shell 10 | git clone https://github.com/2rebi/mysql_udf_http_golang.git udf 11 | cd udf 12 | ``` 13 | 14 | - **Auto Build** 15 | ```shell 16 | bash ./install.sh {username} {password} 17 | ``` 18 | 19 | {username} replace your MySQL or MariaDB Username. 20 | {password} replace your MySQL or MariaDB Password(Optional). 21 | 22 | - **Manual Build** 23 | ```shell 24 | bash ./build.sh 25 | ``` 26 | Build output is `http.so`, move file to `plugin_dir` path. 27 | if you don't know `plugin_dir` path. 28 | Command input this on MySQL, MariaDB connection. 29 | 30 | ```sql 31 | SHOW VARIABLES LIKE 'plugin_dir'; 32 | ``` 33 | 34 | **Ex)** 35 | ```shell 36 | $ mysql -u root -p 37 | Enter password: 38 | ``` 39 | **And** 40 | ```sql 41 | MariaDB [(none)]> SHOW VARIABLES LIKE 'plugin_dir'; 42 | +---------------+-----------------------------------------------+ 43 | | Variable_name | Value | 44 | +---------------+-----------------------------------------------+ 45 | | plugin_dir | /usr/local/Cellar/mariadb/10.3.12/lib/plugin/ | 46 | +---------------+-----------------------------------------------+ 47 | 1 row in set (0.001 sec) 48 | ``` 49 | 50 | and `http.so` move to `Value` path. 51 | ```shell 52 | mv ./http.so /usr/local/Cellar/mariadb/10.3.12/lib/plugin/ 53 | ``` 54 | ### Finally, execute query 55 | 56 | - **Http Help** 57 | ```sql 58 | CREATE FUNCTION http_help RETURNS STRING SONAME 'http.so'; 59 | ``` 60 | - **Http Get Method** 61 | ```sql 62 | CREATE FUNCTION http_get RETURNS STRING SONAME 'http.so'; 63 | ``` 64 | - **Http Post Method** 65 | ```sql 66 | CREATE FUNCTION http_post RETURNS STRING SONAME 'http.so'; 67 | ``` 68 | 69 | 70 | Usage 71 | --- 72 | 73 | ### - Help 74 | 75 | ```sql 76 | SELECT http_help(); 77 | ``` 78 | 79 | ### - GET Method 80 | 81 | - **Prototype** 82 | ```sql 83 | SELECT http_get(url, options...); 84 | ``` 85 | 86 | - **Simple Request** 87 | ```sql 88 | SELECT http_get('http://example.com'); 89 | ``` 90 | **Return** 91 | ```javascript 92 | { 93 | "Body" : String(HTML(Default), Base64, Hexdecimal) 94 | } 95 | ``` 96 | 97 | - **Output Option** 98 | 99 | ```sql 100 | SELECT http_get('http://example.com', '-O FULL'); 101 | ``` 102 | **Return** 103 | ```javascript 104 | { 105 | "Proto" : String(Http Version, HTTP/1.0, HTTP/1.1, HTTP/2.0), 106 | "Status" : String(Status Code, 200 OK, 404 NOT FOUND...), 107 | "Header" : JSON(`{Key : Array, ...}`), 108 | "Body" : String(HTML(Default), Base64, Hexdecimal) 109 | } 110 | ``` 111 | `-O {outputType}` Define kind of result. 112 | `PROTO`, `STATUS` or `STATUS_CODE`, `HEADER`, `BODY`(default), `FULL` 113 | `-O PROTO|STATUS|HEADER|BODY` same this `-O FULL`. 114 | 115 | 116 | - **Custom Header** 117 | 118 | ```sql 119 | SELECT http_get('http://example.com', '-O FULL', '-H CustomKey:CustomValue', '-H Authorization:Bearer a1b2c3d4-123e-5678-9fgh-ijk098765432') 120 | ``` 121 | **Like this** 122 | ```http 123 | GET / HTTP/1.1 124 | Host: example.com 125 | CustomKey: CustomValue 126 | Authorization: Bearer a1b2c3d4-123e-5678-9fgh-ijk098765432 127 | User-Agent: Go-http-client/1.1 128 | Accept-Encoding: gzip 129 | ``` 130 | 131 | Option param input `-H {key}:{value}`. 132 | 133 | ### - POST Method 134 | - **Prototype** 135 | ```sql 136 | SELECT http_post(url, contentType, body, options...) 137 | ``` 138 | - **Simple Request(No Body)** 139 | ```sql 140 | SELECT http_post('http://example.com', '', ''); 141 | ``` 142 | - **Simple Request(Json Body)** 143 | ```sql 144 | SELECT http_post('http://example.com', 'application/json', '{"Hello":"World"}'); 145 | ``` 146 | **Like this** 147 | ```http 148 | POST / HTTP/1.1 149 | Host: example.com 150 | Content-Type: application/json 151 | Content-Length: 17 152 | User-Agent: Go-http-client/1.1 153 | Accept-Encoding: gzip 154 | 155 | 156 | {"Hello":"World"} 157 | ``` 158 | ### - Raw Method 159 | - **Prototype** 160 | ```sql 161 | SELECT http_raw(method, url, body, options...) 162 | ``` 163 | 164 | - **PUT** 165 | ```sql 166 | SELECT http_raw('PUT', url, body, options...) 167 | ``` 168 | - **PATCH** 169 | ```sql 170 | SELECT http_raw('PATCH', url, body, options...) 171 | ``` 172 | - **DELETE** 173 | ```sql 174 | SELECT http_raw('DELETE', url, body, options...) 175 | ``` 176 | 177 | License 178 | --- 179 | [`THE BEER-WARE LICENSE (Revision 42)`](http://en.wikipedia.org/wiki/Beerware) 180 | -------------------------------------------------------------------------------- /http.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // #include 4 | // #include 5 | // #include 6 | // #include 7 | // #include 8 | // #include 9 | import "C" 10 | import ( 11 | "bytes" 12 | "crypto/tls" 13 | "encoding/base64" 14 | "encoding/hex" 15 | "encoding/json" 16 | "errors" 17 | "fmt" 18 | "io" 19 | "io/ioutil" 20 | "net/http" 21 | "strings" 22 | "unicode/utf8" 23 | "unsafe" 24 | ) 25 | 26 | type respResult struct { 27 | Proto string `json:",omitempty"` 28 | Status string `json:",omitempty"` 29 | StatusCode int `json:",omitempty"` 30 | Header map[string][]string `json:",omitempty"` 31 | Body string `json:",omitempty"` 32 | } 33 | 34 | const optionDescription = `option: 35 | -b Define body input type.(hex: hexdecimal output. ex/[ascii]"Hello" -> 48656c6c6f, b64: base64 encoded, txt(default): text) 36 | -B Define body output type.(hex: hexdecimal output. ex/[ascii]"Hello" -> 48656c6c6f, b64: base64 encoded, txt(default): text) 37 | -H Pass custom headers to server (H) 38 | -O Define kind of result.(PROTO, STATUS or STATUS_CODE, HEADER, BODY(default), FULL) ex/-O PROTO|STATUS|HEADER|BODY equal -O FULL 39 | -s Define tls/ssl skip verified true / false 40 | ` 41 | const arrLength = 1 << 30 42 | 43 | func contains(slice []string, str string) bool { 44 | for _, n := range slice { 45 | if n == str { 46 | return true 47 | } 48 | } 49 | // index := sort.SearchStrings(slice, str) 50 | // if index < len(slice) { 51 | // return slice[index] == str 52 | // } 53 | 54 | return false 55 | } 56 | 57 | func httpRaw(method string, url string, contentType string, body string, options []*C.char) (string, error) { 58 | reqHeader := http.Header{} 59 | bodyOption := "txt" 60 | iBodyOption := "txt" 61 | outputOption := "BODY" 62 | sslSkip := false 63 | if options != nil { 64 | for _, opt := range options { 65 | option := strings.Split(C.GoString(opt), " ") 66 | 67 | switch option[0] { 68 | case "-H": 69 | header := strings.Split(strings.Join(option[1:], " "), ":") 70 | if len(header) != 2 { 71 | return "", errors.New("Invalid Header Option") 72 | } 73 | reqHeader.Add(header[0], header[1]) 74 | case "-B": 75 | bodyOption = option[1] 76 | case "-b": 77 | iBodyOption = option[1] 78 | case "-O": 79 | outputOption = option[1] 80 | case "-s": 81 | sslSkip = option[1] == "true" 82 | } 83 | } 84 | } 85 | 86 | var rBody io.Reader 87 | if len(body) > 0 { 88 | switch iBodyOption { 89 | case "txt": 90 | rBody = strings.NewReader(body) 91 | case "b64": 92 | b64Datas, err := base64.StdEncoding.DecodeString(body) 93 | if err != nil { 94 | return "", err 95 | } 96 | rBody = bytes.NewReader(b64Datas) 97 | case "hex": 98 | hexDatas, err := hex.DecodeString(body) 99 | if err != nil { 100 | return "", err 101 | } 102 | rBody = bytes.NewReader(hexDatas) 103 | } 104 | } else { 105 | rBody = nil 106 | } 107 | 108 | req, err := http.NewRequest(method, url, rBody) 109 | if err != nil { 110 | return "", err 111 | } 112 | req.Header = reqHeader 113 | 114 | if rBody != nil && len(contentType) != 0 { 115 | req.Header.Set("Content-Type", contentType) 116 | } 117 | 118 | client := &http.Client{} 119 | if sslSkip { 120 | client.Transport = &http.Transport{ 121 | TLSClientConfig: &tls.Config{ 122 | InsecureSkipVerify: true, 123 | }, 124 | } 125 | } 126 | resp, err := client.Do(req) 127 | if err != nil { 128 | return "", err 129 | } 130 | defer resp.Body.Close() 131 | bytesBody, err := ioutil.ReadAll(resp.Body) 132 | if err != nil { 133 | return "", err 134 | } 135 | 136 | var ret respResult 137 | outputOptions := strings.Split(outputOption, "|") 138 | outputLen := len(outputOptions) 139 | if outputLen == 0 { 140 | return "", errors.New("Invalid Output Option, Zero Option") 141 | } else { 142 | invalidOption := true 143 | if outputLen == 1 && outputOptions[0] == "FULL" { 144 | invalidOption = false 145 | outputOptions = []string{"PROTO", "STATUS", "HEADER", "BODY"} 146 | outputLen = 4 147 | } 148 | 149 | if contains(outputOptions, "PROTO") { 150 | invalidOption = false 151 | ret.Proto = resp.Proto 152 | } 153 | if contains(outputOptions, "STATUS") { 154 | invalidOption = false 155 | ret.Status = resp.Status 156 | } else if contains(outputOptions, "STATUS_CODE") { 157 | invalidOption = false 158 | ret.StatusCode = resp.StatusCode 159 | } 160 | if contains(outputOptions, "HEADER") { 161 | invalidOption = false 162 | ret.Header = resp.Header 163 | } 164 | if contains(outputOptions, "BODY") { 165 | invalidOption = false 166 | switch bodyOption { 167 | case "txt": 168 | ret.Body = string(bytesBody) 169 | case "b64": 170 | ret.Body = base64.StdEncoding.EncodeToString(bytesBody) 171 | case "hex": 172 | ret.Body = hex.EncodeToString(bytesBody) 173 | default: 174 | return "", errors.New("Invalid Body Option") 175 | } 176 | } 177 | 178 | if invalidOption { 179 | return "", errors.New("Invalid Output Option, " + fmt.Sprintf("(%v)", outputOptions)) 180 | } 181 | } 182 | 183 | jBuffer := &bytes.Buffer{} 184 | encoder := json.NewEncoder(jBuffer) 185 | encoder.SetEscapeHTML(false) 186 | err = encoder.Encode(ret) 187 | if err != nil { 188 | return "", err 189 | } 190 | 191 | return string(jBuffer.Bytes()), nil 192 | } 193 | 194 | //export http_raw_init 195 | func http_raw_init(initid *C.UDF_INIT, args *C.UDF_ARGS, message *C.char) C.my_bool { 196 | if args.arg_count < 3 { 197 | msg := ` 198 | http_raw(method string, url string, body string, option ...string) requires method, url, body argment 199 | ` + optionDescription 200 | C.strcpy(message, C.CString(msg)) 201 | return 1 202 | } 203 | return 0 204 | } 205 | 206 | //export http_raw 207 | func http_raw(initid *C.UDF_INIT, args *C.UDF_ARGS, result *C.char, length *uint64, 208 | null_value *C.char, message *C.char) *C.char { 209 | gArg_count := uint(args.arg_count) 210 | 211 | var ret string 212 | var err error 213 | gArgs := ((*[arrLength]*C.char)(unsafe.Pointer(args.args)))[:gArg_count:gArg_count] 214 | method := C.GoString(*args.args) 215 | switch method { 216 | case "GET": 217 | if gArg_count == 3 { 218 | ret, err = httpRaw(method, C.GoString(gArgs[1]), "", "", nil) 219 | } else { 220 | ret, err = httpRaw(method, C.GoString(gArgs[1]), "", "", gArgs[3:]) 221 | } 222 | default: 223 | if gArg_count == 3 { 224 | ret, err = httpRaw(method, C.GoString(gArgs[1]), "", C.GoString(gArgs[2]), nil) 225 | } else { 226 | ret, err = httpRaw(method, C.GoString(gArgs[1]), "", C.GoString(gArgs[2]), gArgs[3:]) 227 | } 228 | } 229 | 230 | if err != nil { 231 | ret = err.Error() 232 | } 233 | 234 | result = C.CString(ret) 235 | *length = uint64(utf8.RuneCountInString(ret)) 236 | return result 237 | } 238 | 239 | //export http_get_init 240 | func http_get_init(initid *C.UDF_INIT, args *C.UDF_ARGS, message *C.char) C.my_bool { 241 | if args.arg_count == 0 { 242 | msg := ` 243 | http_get(url string, option ...string) requires url argment 244 | ` + optionDescription 245 | C.strcpy(message, C.CString(msg)) 246 | return 1 247 | } 248 | 249 | return 0 250 | } 251 | 252 | //export http_get 253 | func http_get(initid *C.UDF_INIT, args *C.UDF_ARGS, result *C.char, length *uint64, 254 | null_value *C.char, message *C.char) *C.char { 255 | gArg_count := uint(args.arg_count) 256 | 257 | var ret string 258 | var err error 259 | if gArg_count == 1 { 260 | ret, err = httpRaw("GET", C.GoString(*args.args), "", "", nil) 261 | } else { 262 | gArgs := ((*[arrLength]*C.char)(unsafe.Pointer(args.args)))[:gArg_count:gArg_count] 263 | ret, err = httpRaw("GET", C.GoString(*args.args), "", "", gArgs[1:]) 264 | } 265 | 266 | if err != nil { 267 | ret = err.Error() 268 | } 269 | 270 | result = C.CString(ret) 271 | *length = uint64(utf8.RuneCountInString(ret)) 272 | return result 273 | } 274 | 275 | //export http_post_init 276 | func http_post_init(initid *C.UDF_INIT, args *C.UDF_ARGS, message *C.char) C.my_bool { 277 | if args.arg_count < 3 { 278 | msg := ` 279 | http_post(url string, contentType string, body string, option ...string) requires url, contentType, body argment 280 | ` + optionDescription 281 | C.strcpy(message, C.CString(msg)) 282 | return 1 283 | } 284 | return 0 285 | } 286 | 287 | //export http_post 288 | func http_post(initid *C.UDF_INIT, args *C.UDF_ARGS, result *C.char, length *uint64, 289 | null_value *C.char, message *C.char) *C.char { 290 | gArg_count := uint(args.arg_count) 291 | 292 | var ret string 293 | var err error 294 | gArgs := ((*[arrLength]*C.char)(unsafe.Pointer(args.args)))[:gArg_count:gArg_count] 295 | if gArg_count == 3 { 296 | ret, err = httpRaw("POST", C.GoString(*args.args), C.GoString(gArgs[1]), C.GoString(gArgs[2]), nil) 297 | } else { 298 | ret, err = httpRaw("POST", C.GoString(*args.args), C.GoString(gArgs[1]), C.GoString(gArgs[2]), gArgs[3:]) 299 | } 300 | 301 | if err != nil { 302 | ret = err.Error() 303 | } 304 | 305 | result = C.CString(ret) 306 | *length = uint64(utf8.RuneCountInString(ret)) 307 | return result 308 | } 309 | 310 | //export http_help_init 311 | func http_help_init(initid *C.UDF_INIT, args *C.UDF_ARGS, message *C.char) C.my_bool { 312 | return 0 313 | } 314 | 315 | //export http_help 316 | func http_help(initid *C.UDF_INIT, args *C.UDF_ARGS, result *C.char, length *uint64, 317 | null_value *C.char, message *C.char) *C.char { 318 | 319 | msg := ` 320 | Method List. 321 | http_raw(method string, url string, body string, option ...string) requires method, url, body argment 322 | http_get(url string, option ...string) requires url argment 323 | http_post(url string, contentType string, body string, option ...string) requires url, contentType, body argment 324 | 325 | ` + optionDescription 326 | 327 | result = C.CString(msg) 328 | *length = uint64(utf8.RuneCountInString(msg)) 329 | return result 330 | } 331 | 332 | func main() { 333 | } 334 | --------------------------------------------------------------------------------