Technology Engineering

178inaba の技術ブログ

ConoHaでサーバ立ててユーザ追加したりSSHの設定したり

inabaです。

ConoHaが安くてサーバ立ち上げが早くて良さげです。

www.conoha.jp

なのでVPSを借りて、いじって遊ぶ用のサーバを作ります。

参考記事:

qiita.com

大体は上記に書いてある通りに進めます。
使用するOSはCentOS 7です。
ここには気になった事や追加でやった事等を書いていきます。

wheelグループって何?

wheelグループに追加と書かれている箇所があります。

# wheelグループに追加
$ gpasswd -a newuser wheel

wheelグループとはデフォルトでsudoができるグループです。
所属しているとユーザ個別に設定しなくてもsudoができます。 設定は/etc/sudoersに記述されています。

$ cat /etc/sudoers
...
## Allows people in group wheel to run all commands
%wheel  ALL=(ALL)   ALL
...

gpasswd

いつもグループへの追加はusermodでやっていて、gpasswdは知りませんでした。
usermod-a忘れるとグループが置き換えられる事がありますが、gpasswdはその心配が無いので良さげです。

qiita.com

パスワード無しでsudo

先程の/etc/sudoersのwheelグループの設定の下にパスワード無し用の設定がコメントアウトされているのでそのコメントアウトを外し、元の設定をコメントアウトします。
sudoは結構重要な操作なので自分は一旦パスワード有りで使ってみて、面倒になってきたら外そうと思います。

## Allows people in group wheel to run all commands
# %wheel    ALL=(ALL)   ALL

## Same thing without a password
%wheel  ALL=(ALL)   NOPASSWD: ALL

SSHログイン許可の設定はデフォルトを使う

参考サイトのSSHログインを許可の項目ですが、設定ファイル(/etc/ssh/sshd_config)はデフォルトだとこうなっていました。

#RSAAuthentication yes
#PubkeyAuthentication yes
AuthorizedKeysFile  .ssh/authorized_keys

RSAAuthenticationPubkeyAuthenticationコメントアウトされています。
コメントアウトされているという事はデフォルト値が使われるという事。

ここにデフォルト値が書いてあるのですが、

sshd の設定(sshd_config)

RSAAuthenticationPubkeyAuthenticationのデフォルト値はyesです。
自分は極力デフォルトで行けるならデフォルトを使いたい人間なので、設定はいじらず、コメントアウトのまま使います。

SSHキーはローカルにあるものを使う

SSH鍵を作成の項目でホスト側でキーを新たに作ってローカルに秘密鍵をコピーしていますが、自分はローカルにSSHキーが生成してあるのでこれを使います。

ローカル:

$ cat ~/.ssh/id_rsa.pub # 公開鍵
ssh-rsa foobarbaz...
# 出た文字列をコピー

サーバ(rootではなく、新しく作ったユーザで実行。):

$ ssh-keygen # ~/.sshディレクトリを手で作るのが面倒なのでコマンドで作る。
# デフォルトで良いのでEnter3回

$ echo 'ssh-rsa foobarbaz...' > ~/.ssh/authorized_keys # ローカルでコピーした公開鍵を登録

$ chmod 600 ~/.ssh/authorized_keys # パーミッションを設定

ssh-keygenを使っちゃってますが、これは補完が効くコマンドの方が打つのが楽だから使ってます。
作った鍵は今は使わないので、普通にmkdir ~/.sshでもいいです。

authorized_keysパーミッションの設定が重要

これで結構ハマった。
SSHキーを使ってでログインしようとしても、パスワードを聞かれるので、sshのログが書かれる /var/log/secureあたりを調べて

Authentication refused: bad ownership or modes for file /home/newuser/.ssh/authorized_keys

と出てきたのでこれで検索。

takuya-1st.hatenablog.jp

authorized_keysパーミッションオーナー以外の書き込み権限がついているとダメなようだった。
実際にauthorized_keysパーミッションを見てみるとrw-rw-r–になっていたので上記の通りchmod 600 ~/.ssh/authorized_keysパーミッションを設定してやると、パスワードを聞かれなくなった!

ログイン攻撃がヤバい

lastbでログイン失敗履歴が見られるのだが、身に覚えのない失敗が約1000回くらいあった。
怖くなってlastでログイン成功履歴を見たが、そっちには身に覚えの無い履歴は無かったので良かったが、こんなに攻撃されるのかとビビるね。
精神衛生上良くない。

なので、rootからのログインを禁止パスワードによるログインを禁止は絶対やるべき。

/etc/ssh/sshd_config:

PermitRootLogin no
PasswordAuthentication no

まとめ

何点か書きましたが、sshauthorized_keysパーミッション以外は特にハマること無く進められました。
このサーバで何しよっかな〜。

今日はここまで。

bufio.Scannerのエラーと解決策 #golang

こんばんわ。
inabaです。

今日はAtCoder Beginner Contest 055に参加していました。

その時に起こったエラーを調査したので参考までに。

Runtime Error

ある問題を解いて提出したら何件かでRuntime Errorが出た。
Runtime Errorの内容は見られないのでコードをちょこちょこ変えながらデバッグ

原因

読み込み文字列が長いのが原因。
その問題の前提条件には10万文字が最大で入ってくるという事が書いてあったが、bufio.Scannerがデフォルトで最大サイズが決まっている事を知らずハマってた。

最大サイズの定数:

MaxScanTokenSize = 64 * 1024

64 * 1024だから約6万5千文字がデフォルトで読み込める最大文字数。
10万文字なので余裕で超える。
デフォルトを超えるサイズの文字列を(*Scanner).Scanすると(*Scanner).Errbufio.ErrTooLongが返される。

var sc = bufio.NewScanner(os.Stdin)
sc.Split(bufio.ScanWords)
sc.Scan()
if err := sc.Err(); err == bufio.ErrTooLong {
    panic(err)
}

みたいな感じで調査してやっと見つけた。

解決策

(*Scanner).Bufferで最大サイズを指定すると65536文字より大きい文字列も読み込んでくれた。

var sc = bufio.NewScanner(os.Stdin)
sc.Buffer([]byte{}, math.MaxInt64)

こんな感じです。
とりあえずint64の最大値を指定してますが、いきなり指定サイズまでアロケーションされるという事は無く、現行のサイズ*2で読み込みを再試行していくようなのでまぁいいかなと。

src/bufio/scan.go - The Go Programming Language

今日はここまで。

外部キーが設定されているとTRUNCATEできない #MariaDB #MySQL

inabaです。

最近はgolangでDBをいじっています。
テストもテスト用DBを作り、実際に書き込んで行うようにしていました。

テストをするたびにAUTO_INCREMENTの値が増えていく

まぁそうでしょう。
なので、テストデータのクリアをするためにTRUNCATEしようと思ってエラー出てハマりました。

エラー

2つのエラーが出ました。

プレースホルダ

_, err = db.Exec("TRUNCATE TABLE ?", tableName)

上記コードの時は下記のエラーが出ました。

Error 1064: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near '?' at line 1

テーブル名にプレースホルダは使えないらしい。
元々プレースホルダはキャッシュ目的だからFROM句は変えないですし、
セキュリティ的な使用でも、ユーザの入力をFROM句には使わないから妥当な動きですよね。

外部キー制約

プレースホルダをやめてfmt.Sprintfを使ってやるようにしてみたらエラー内容が変わりました。

_, err = db.Exec(fmt.Sprintf("TRUNCATE TABLE %s", tableName))

下記がエラー。

Error 1701: Cannot truncate a table referenced in a foreign key constraint ...

これは、外部キー制約によってTRUNCATEができないというエラーでした。

外部キー制約の回避

外部キー制約を回避する方法があります。

_, err = db.Exec("SET FOREIGN_KEY_CHECKS = 0")
_, err = db.Exec(fmt.Sprintf("TRUNCATE TABLE %s", tableName))
_, err = db.Exec("SET FOREIGN_KEY_CHECKS = 1")

上記コードではエラーは出ません。
FOREIGN_KEY_CHECKS0に設定すると、外部キー制約の回避ができます。

別の解決法

AUTO_INCREMENTが初期化されればよいのであれば、DELETEしてからAUTO_INCREMENTをリセットすることもできます。

_, err = db.Exec(fmt.Sprintf("DELETE FROM %s", tableName))
_, err = db.Exec(fmt.Sprintf("ALTER TABLE %s AUTO_INCREMENT = 1", tableName))

これなら2行で済みます。
ただ、子テーブルにデータが入っているとエラーになるので、テストで子テーブルも使っている場合は子テーブルの方から消していってください。

今日はここまで。

【Go】structをJSONにする時、ゼロ値を含む/含まないを切り替える方法と注意点 #golang

inabaです。

Go言語でstructをJSONにする時、ゼロ値を含まない場合はタグにomitemptyをつけますよね。

type stampCard struct {
    Stamp int `json:"stamp,omitempty"`
}

しかし、同じstructを使っている時にゼロ値を含む/含まないを切り替えたい時があります。
具体的には下記のようなstructです。

type stampCard struct {
    InStampRally bool `json:"in_stamp_rally"`
    Stamp        int  `json:"stamp,omitempty"`
}

上記のstructではInStampRallyがtrueの時だけStampの値をJSONに含めたいです。
しかし、このstructのままだとInStampRallyがtrueでもStampが0なら含まれません。

package main

import (
    "encoding/json"
    "fmt"

    log "github.com/Sirupsen/logrus"
)

type stampCard struct {
    InStampRally bool `json:"in_stamp_rally"`
    Stamp        int  `json:"stamp,omitempty"`
}

func main() {
    sc := stampCard{InStampRally: true, Stamp: 0}
    jsc, err := json.Marshal(pc)
    if err != nil {
        log.Error(err)
    }

    fmt.Println(string(jsc)) // {"in_stamp_rally":true}
}

解決策

解決策はゼロ値を含む/含まないを切り替えたい値をポインタ化します。

type stampCard struct {
    InStampRally bool `json:"in_stamp_rally"`
    Stamp        *int `json:"stamp,omitempty"`
}

これで

  • nilでないならJSONに値を含める
  • nilならJSONに値を含めない

という切り替えができます。

package main

import (
    "encoding/json"
    "fmt"

    log "github.com/Sirupsen/logrus"
)

type stampCard struct {
    InStampRally bool `json:"in_stamp_rally"`
    Stamp        *int `json:"stamp,omitempty"`
}

func main() {
    scs := []stampCard{
        {InStampRally: false},
        {InStampRally: true},
    }

    for i, sc := range scs {
        if sc.InStampRally {
            scs[i].Stamp = intPointer(0)
        }
    }

    jscs, err := json.Marshal(scs)
    if err != nil {
        log.Error(err)
    }

    fmt.Println(string(jscs)) // [{"in_stamp_rally":false},{"in_stamp_rally":true,"stamp":0}]
}

func intPointer(v int) *int {
    return &v
}

上記サンプルコードではstampCardのスライスをループさせてInStampRallyがtrueなら値が0のintポインターStampに代入しています。
こうすることで下記のようにomitemptyが指定されていてもゼロ値をJSONに出力することができます。

[
  {
    "in_stamp_rally": false
  },
  {
    "in_stamp_rally": true,
    "stamp": 0
  }
]

注意点

rangeループのvalueからポインターを取ると、ループでvalueの値が変わってもポインターの値は同じなので、注意しないと意図しない値が入ってしまいます。
(ポインターの値が同じ理由は、ループのたびに新しいメモリを確保しないためでしょう。)

DBから取得した値をループさせてJSON化する時等、注意が必要です。

例として、下記は順番に0, 1, 2, 3のスタンプ数のスタンプカードができる事を期待しているコードです。

package main

import (
    "encoding/json"
    "fmt"

    log "github.com/Sirupsen/logrus"
)

type stampCard struct {
    InStampRally bool `json:"in_stamp_rally"`
    Stamp        *int `json:"stamp,omitempty"`
}

func main() {
    stamps := []int{0, 1, 2, 3}

    var scs []stampCard
    for _, stamp := range stamps {
        log.Infof("%p", &stamp)
        sc := stampCard{InStampRally: true, Stamp: &stamp}
        scs = append(scs, sc)
    }

    jscs, err := json.Marshal(scs)
    if err != nil {
        log.Error(err)
    }

    fmt.Println(string(jscs))
}

しかし、上記の結果は下記になります。

$ go run main.go | jq
INFO[0000] 0xc42000a3d0
INFO[0000] 0xc42000a3d0
INFO[0000] 0xc42000a3d0
INFO[0000] 0xc42000a3d0
[
  {
    "in_stamp_rally": true,
    "stamp": 3
  },
  {
    "in_stamp_rally": true,
    "stamp": 3
  },
  {
    "in_stamp_rally": true,
    "stamp": 3
  },
  {
    "in_stamp_rally": true,
    "stamp": 3
  }
]

まず、ループ中のvalueのポインター値を表示していますが、すべて同じになっています。
その後のJSONは全てのstamp3になってしまいました。
全ての値が最後の値に書き換わってしまっています。

修正後のコード

ループのvalueを一旦新しい変数に入れてその変数のポインターを取れば正確な値をJSONに出力できます。

package main

import (
    "encoding/json"
    "fmt"

    log "github.com/Sirupsen/logrus"
)

type stampCard struct {
    InStampRally bool `json:"in_stamp_rally"`
    Stamp        *int `json:"stamp,omitempty"`
}

func main() {
    stamps := []int{0, 1, 2, 3}

    var scs []stampCard
    for _, stamp := range stamps {
        tmp := stamp // important!!
        sc := stampCard{InStampRally: true, Stamp: &tmp}
        scs = append(scs, sc)
    }

    jscs, err := json.Marshal(scs)
    if err != nil {
        log.Error(err)
    }

    fmt.Println(string(jscs))
}

結果:

[
  {
    "in_stamp_rally": true,
    "stamp": 0
  },
  {
    "in_stamp_rally": true,
    "stamp": 1
  },
  {
    "in_stamp_rally": true,
    "stamp": 2
  },
  {
    "in_stamp_rally": true,
    "stamp": 3
  }
]

今日はここまで。

npm loginは空いているユーザ名を指定するとアカウントを作成する

inabaです。

npm owner addで自分のパッケージにオーナーを追加できると知り、
試すためにログインが必要だったのでnpm loginでログインした時の話。

$ npm login
Username: foobar
Password: 
Email: (this IS public) foobar@example.com

上記を見てもらうとわかりますが、ユーザ名、パスワードの次にEmailの入力があります。
「なんでユーザ名入力してるのにEmailの入力も必要なのかな〜」という疑問が湧いてきたので調べてみた。

実はnpm loginは取得されていないユーザ名を指定すると新しいアカウントを作る挙動になっていた。

github.com

loginという名前だったのでこの挙動にはちょっと驚いた。

loginはnpm adduserエイリアスなんだそう。
上記issueではloginとadduserは別々のコマンドに分割する方向で考えているというような事が書いてあった。

今日はここまで。

git mergeのとき、デフォルトでno-ffになるようにする設定

inabaです。

ブランチのマージ時にマージ対象のブランチでどんな変更があったか知るためにマージコミットが欲しいので--no-ff(No Fast-forward)オプションをつけていました。

$ git merge --no-ff foobar

ですが、設定でデフォルトをno-ffにできることを知りました。

設定

$ git config --global merge.ff false

上記設定をすることでgit merge foobarのみでno-ffのマージになります。

$ git merge foobar
...

$ git log --graph --oneline
*   f84d525 Merge branch 'foobar'
|\  
| * 2d68cba Fix B.
|/  
* a3159f3 Fix A.

参考サイト: Git - git-merge Documentation

問題

しかし、上記のみだとpull時もno-ffでマージコミットができてしまいます。

$ git pull
...

$ git log --graph --oneline
*   73018f7 Merge branch 'master' of github.com:178inaba/test
|\  
| *   f84d525 Merge branch 'foobar'
| |\  
|/ /  
| * 2d68cba Fix B.
|/  
* a3159f3 Fix A.

よく言われることですがpullというのはfetchとリモートブランチのmergeの組み合わせなのでリモートブランチをマージする時に上記no-ffの設定が適用されてしまいます。
これを防ぐためにpull時にはFast-forwardでマージするように設定します。

$ git config --global pull.ff only
$ git log --graph --oneline 
* c7356cb Fix C.

$ git pull
...

$ git log --graph --oneline 
* bdbb39c Fix D.
* c7356cb Fix C.

pullでマージコミットが作られないようになりました。

まとめ

$ git config --global merge.ff false
$ git config --global pull.ff only
$ cat ~/.gitconfig
[merge]
    ff = false
[pull]
    ff = only

今日はここまで。

HomebrewでインストールしたMariaDBをStrictモードに設定

inabaです。

MariaDBをHomebrewでインストールして使っていたのですが、
NOT NULL制約のカラムにDEFAULT NULLが設定されていると、INSERT時に値を設定していないカラムにNULLが入ってしまうという事がありました。
これはStrictモードが設定されていると起きないそうなので、Strictモードの設定をします。

デフォルトのsql_mode

Strictモードはmysqlにログイン後、sql_modeの値で設定されているか確認できます。

MariaDB [(none)]> SELECT @@SQL_MODE, @@GLOBAL.SQL_MODE;
+--------------------------------------------+--------------------------------------------+
| @@SQL_MODE                                 | @@GLOBAL.SQL_MODE                          |
+--------------------------------------------+--------------------------------------------+
| NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION | NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION |
+--------------------------------------------+--------------------------------------------+
1 row in set (0.00 sec)

デフォルトではNO_AUTO_CREATE_USERNO_ENGINE_SUBSTITUTIONが設定されており、
Strictモードの設定値であるSTRICT_ALL_TABLESまたはSTRICT_TRANS_TABLESが設定されていません。

my.cnfのパス

my.cnfのパスを調べます。

mariadb.com

上記には/etc/my.cnfが一番上に書いてありますが、/etc/my.cnfに設定ファイルを置いてみても反映されませんでした。

qiita.com

上記の方法を試します。

$ mysql --help | grep my.cnf
/usr/local/etc/my.cnf ~/.my.cnf 
                      order of preference, my.cnf, $MYSQL_TCP_PORT,

$ cat /usr/local/etc/my.cnf
#
# This group is read both both by the client and the server
# use it for options that affect everything
#
[client-server]

#
# include all files from the config directory
#
!includedir /usr/local/etc/my.cnf.d

/usr/local/etc/my.cnfに設定ファイルmy.cnfがありました。

設定

先程の設定に!includedir /usr/local/etc/my.cnf.dとあるので、/usr/local/etc/my.cnfを直接編集するのではなく、/usr/local/etc/my.cnf.dに設定ファイルを新しく作るようにします。

設定値にはデフォルトで設定されていたNO_AUTO_CREATE_USERNO_ENGINE_SUBSTITUTIONも含めておきます。
含めないとデフォルトで設定されていた上記2つは消えてしまいます。

Strictモードの設定値はSTRICT_ALL_TABLESSTRICT_TRANS_TABLESがあります。
違いについてはDifference between strict_all_tables and strict_trans_tablesがわかりやすいです。
自分は間違った値が入ってほしくなかったのでSTRICT_ALL_TABLESを使用するようにしました。
InnoDBだと違いは無いようなのでInnoDBの場合はどちらを選んでも良いでしょう。

$ emacs /usr/local/etc/my.cnf.d/my.cnf
$ cat /usr/local/etc/my.cnf.d/my.cnf 
[mysqld]
sql_mode = NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION,STRICT_ALL_TABLES

# MariaDBの再起動
$ mysql.server restart
Shutting down MySQL
.. SUCCESS! 
Starting MySQL
.170105 01:51:36 mysqld_safe Logging to '/usr/local/var/mysql/i.local.err'.
 SUCCESS! 

設定値を確認してみます。

MariaDB [(none)]> SELECT @@SQL_MODE, @@GLOBAL.SQL_MODE;
+--------------------------------------------------------------+--------------------------------------------------------------+
| @@SQL_MODE                                                   | @@GLOBAL.SQL_MODE                                            |
+--------------------------------------------------------------+--------------------------------------------------------------+
| STRICT_ALL_TABLES,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION | STRICT_ALL_TABLES,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION |
+--------------------------------------------------------------+--------------------------------------------------------------+
1 row in set (0.00 sec)

STRICT_ALL_TABLESが設定されている事が確認できました。

これでNOT NULL制約が厳密に適用されます。

今日はここまで。